Skip to content

Commit eebc24b

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#26887: RPC: make RPCResult::MatchesType return useful errors
3d1a4d8 RPC: make RPCResult::MatchesType return useful errors (Anthony Towns) Pull request description: Currently if you don't correctly update the description of the return value for an RPC call, you essentially just get an assertion failure with no useful information; this generates a description of the problems instead. ACKs for top commit: MarcoFalke: re-ACK 3d1a4d8 🌷 Tree-SHA512: cf0580b7046faab0128672a74f8cc5a1655dfdca6646a2e38b51f0fb5f672c98aad6cd4c5769454a2d644a67da639ccb1c8ff5d24d3d6b4446a082398a643722
2 parents 58da161 + 3d1a4d8 commit eebc24b

File tree

2 files changed

+77
-31
lines changed

2 files changed

+77
-31
lines changed

src/rpc/util.cpp

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5+
#include <clientversion.h>
56
#include <consensus/amount.h>
67
#include <key_io.h>
78
#include <outputtype.h>
@@ -568,7 +569,26 @@ UniValue RPCHelpMan::HandleRequest(const JSONRPCRequest& request) const
568569
}
569570
UniValue ret = m_fun(*this, request);
570571
if (gArgs.GetBoolArg("-rpcdoccheck", DEFAULT_RPC_DOC_CHECK)) {
571-
CHECK_NONFATAL(std::any_of(m_results.m_results.begin(), m_results.m_results.end(), [&ret](const RPCResult& res) { return res.MatchesType(ret); }));
572+
UniValue mismatch{UniValue::VARR};
573+
for (const auto& res : m_results.m_results) {
574+
UniValue match{res.MatchesType(ret)};
575+
if (match.isTrue()) {
576+
mismatch.setNull();
577+
break;
578+
}
579+
mismatch.push_back(match);
580+
}
581+
if (!mismatch.isNull()) {
582+
std::string explain{
583+
mismatch.empty() ? "no possible results defined" :
584+
mismatch.size() == 1 ? mismatch[0].write(4) :
585+
mismatch.write(4)};
586+
throw std::runtime_error{
587+
strprintf("Internal bug detected: RPC call \"%s\" returned incorrect type:\n%s\n%s %s\nPlease report this issue here: %s\n",
588+
m_name, explain,
589+
PACKAGE_NAME, FormatFullVersion(),
590+
PACKAGE_BUGREPORT)};
591+
}
572592
}
573593
return ret;
574594
}
@@ -883,53 +903,77 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const
883903
NONFATAL_UNREACHABLE();
884904
}
885905

886-
bool RPCResult::MatchesType(const UniValue& result) const
906+
static const std::optional<UniValue::VType> ExpectedType(RPCResult::Type type)
887907
{
888-
if (m_skip_type_check) {
889-
return true;
890-
}
891-
switch (m_type) {
908+
using Type = RPCResult::Type;
909+
switch (type) {
892910
case Type::ELISION:
893911
case Type::ANY: {
894-
return true;
912+
return std::nullopt;
895913
}
896914
case Type::NONE: {
897-
return UniValue::VNULL == result.getType();
915+
return UniValue::VNULL;
898916
}
899917
case Type::STR:
900918
case Type::STR_HEX: {
901-
return UniValue::VSTR == result.getType();
919+
return UniValue::VSTR;
902920
}
903921
case Type::NUM:
904922
case Type::STR_AMOUNT:
905923
case Type::NUM_TIME: {
906-
return UniValue::VNUM == result.getType();
924+
return UniValue::VNUM;
907925
}
908926
case Type::BOOL: {
909-
return UniValue::VBOOL == result.getType();
927+
return UniValue::VBOOL;
910928
}
911929
case Type::ARR_FIXED:
912930
case Type::ARR: {
913-
if (UniValue::VARR != result.getType()) return false;
931+
return UniValue::VARR;
932+
}
933+
case Type::OBJ_DYN:
934+
case Type::OBJ: {
935+
return UniValue::VOBJ;
936+
}
937+
} // no default case, so the compiler can warn about missing cases
938+
NONFATAL_UNREACHABLE();
939+
}
940+
941+
UniValue RPCResult::MatchesType(const UniValue& result) const
942+
{
943+
if (m_skip_type_check) {
944+
return true;
945+
}
946+
947+
const auto exp_type = ExpectedType(m_type);
948+
if (!exp_type) return true; // can be any type, so nothing to check
949+
950+
if (*exp_type != result.getType()) {
951+
return strprintf("returned type is %s, but declared as %s in doc", uvTypeName(result.getType()), uvTypeName(*exp_type));
952+
}
953+
954+
if (UniValue::VARR == result.getType()) {
955+
UniValue errors(UniValue::VOBJ);
914956
for (size_t i{0}; i < result.get_array().size(); ++i) {
915957
// If there are more results than documented, re-use the last doc_inner.
916958
const RPCResult& doc_inner{m_inner.at(std::min(m_inner.size() - 1, i))};
917-
if (!doc_inner.MatchesType(result.get_array()[i])) return false;
959+
UniValue match{doc_inner.MatchesType(result.get_array()[i])};
960+
if (!match.isTrue()) errors.pushKV(strprintf("%d", i), match);
918961
}
919-
return true; // empty result array is valid
962+
if (errors.empty()) return true; // empty result array is valid
963+
return errors;
920964
}
921-
case Type::OBJ_DYN:
922-
case Type::OBJ: {
923-
if (UniValue::VOBJ != result.getType()) return false;
965+
966+
if (UniValue::VOBJ == result.getType()) {
924967
if (!m_inner.empty() && m_inner.at(0).m_type == Type::ELISION) return true;
968+
UniValue errors(UniValue::VOBJ);
925969
if (m_type == Type::OBJ_DYN) {
926970
const RPCResult& doc_inner{m_inner.at(0)}; // Assume all types are the same, randomly pick the first
927971
for (size_t i{0}; i < result.get_obj().size(); ++i) {
928-
if (!doc_inner.MatchesType(result.get_obj()[i])) {
929-
return false;
930-
}
972+
UniValue match{doc_inner.MatchesType(result.get_obj()[i])};
973+
if (!match.isTrue()) errors.pushKV(result.getKeys()[i], match);
931974
}
932-
return true; // empty result obj is valid
975+
if (errors.empty()) return true; // empty result obj is valid
976+
return errors;
933977
}
934978
std::set<std::string> doc_keys;
935979
for (const auto& doc_entry : m_inner) {
@@ -939,26 +983,26 @@ bool RPCResult::MatchesType(const UniValue& result) const
939983
result.getObjMap(result_obj);
940984
for (const auto& result_entry : result_obj) {
941985
if (doc_keys.find(result_entry.first) == doc_keys.end()) {
942-
return false; // missing documentation
986+
errors.pushKV(result_entry.first, "key returned that was not in doc");
943987
}
944988
}
945989

946990
for (const auto& doc_entry : m_inner) {
947991
const auto result_it{result_obj.find(doc_entry.m_key_name)};
948992
if (result_it == result_obj.end()) {
949993
if (!doc_entry.m_optional) {
950-
return false; // result is missing a required key
994+
errors.pushKV(doc_entry.m_key_name, "key missing, despite not being optional in doc");
951995
}
952996
continue;
953997
}
954-
if (!doc_entry.MatchesType(result_it->second)) {
955-
return false; // wrong type
956-
}
998+
UniValue match{doc_entry.MatchesType(result_it->second)};
999+
if (!match.isTrue()) errors.pushKV(doc_entry.m_key_name, match);
9571000
}
958-
return true;
1001+
if (errors.empty()) return true;
1002+
return errors;
9591003
}
960-
} // no default case, so the compiler can warn about missing cases
961-
NONFATAL_UNREACHABLE();
1004+
1005+
return true;
9621006
}
9631007

9641008
void RPCResult::CheckInnerDoc() const

src/rpc/util.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,10 @@ struct RPCResult {
324324
std::string ToStringObj() const;
325325
/** Return the description string, including the result type. */
326326
std::string ToDescriptionString() const;
327-
/** Check whether the result JSON type matches. */
328-
bool MatchesType(const UniValue& result) const;
327+
/** Check whether the result JSON type matches.
328+
* Returns true if type matches, or object describing error(s) if not.
329+
*/
330+
UniValue MatchesType(const UniValue& result) const;
329331

330332
private:
331333
void CheckInnerDoc() const;

0 commit comments

Comments
 (0)