Skip to content

Commit 3d1a4d8

Browse files
committed
RPC: make RPCResult::MatchesType return useful errors
1 parent f4ef856 commit 3d1a4d8

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>
@@ -581,7 +582,26 @@ UniValue RPCHelpMan::HandleRequest(const JSONRPCRequest& request) const
581582
}
582583
UniValue ret = m_fun(*this, request);
583584
if (gArgs.GetBoolArg("-rpcdoccheck", DEFAULT_RPC_DOC_CHECK)) {
584-
CHECK_NONFATAL(std::any_of(m_results.m_results.begin(), m_results.m_results.end(), [&ret](const RPCResult& res) { return res.MatchesType(ret); }));
585+
UniValue mismatch{UniValue::VARR};
586+
for (const auto& res : m_results.m_results) {
587+
UniValue match{res.MatchesType(ret)};
588+
if (match.isTrue()) {
589+
mismatch.setNull();
590+
break;
591+
}
592+
mismatch.push_back(match);
593+
}
594+
if (!mismatch.isNull()) {
595+
std::string explain{
596+
mismatch.empty() ? "no possible results defined" :
597+
mismatch.size() == 1 ? mismatch[0].write(4) :
598+
mismatch.write(4)};
599+
throw std::runtime_error{
600+
strprintf("Internal bug detected: RPC call \"%s\" returned incorrect type:\n%s\n%s %s\nPlease report this issue here: %s\n",
601+
m_name, explain,
602+
PACKAGE_NAME, FormatFullVersion(),
603+
PACKAGE_BUGREPORT)};
604+
}
585605
}
586606
return ret;
587607
}
@@ -860,53 +880,77 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const
860880
NONFATAL_UNREACHABLE();
861881
}
862882

863-
bool RPCResult::MatchesType(const UniValue& result) const
883+
static const std::optional<UniValue::VType> ExpectedType(RPCResult::Type type)
864884
{
865-
if (m_skip_type_check) {
866-
return true;
867-
}
868-
switch (m_type) {
885+
using Type = RPCResult::Type;
886+
switch (type) {
869887
case Type::ELISION:
870888
case Type::ANY: {
871-
return true;
889+
return std::nullopt;
872890
}
873891
case Type::NONE: {
874-
return UniValue::VNULL == result.getType();
892+
return UniValue::VNULL;
875893
}
876894
case Type::STR:
877895
case Type::STR_HEX: {
878-
return UniValue::VSTR == result.getType();
896+
return UniValue::VSTR;
879897
}
880898
case Type::NUM:
881899
case Type::STR_AMOUNT:
882900
case Type::NUM_TIME: {
883-
return UniValue::VNUM == result.getType();
901+
return UniValue::VNUM;
884902
}
885903
case Type::BOOL: {
886-
return UniValue::VBOOL == result.getType();
904+
return UniValue::VBOOL;
887905
}
888906
case Type::ARR_FIXED:
889907
case Type::ARR: {
890-
if (UniValue::VARR != result.getType()) return false;
908+
return UniValue::VARR;
909+
}
910+
case Type::OBJ_DYN:
911+
case Type::OBJ: {
912+
return UniValue::VOBJ;
913+
}
914+
} // no default case, so the compiler can warn about missing cases
915+
NONFATAL_UNREACHABLE();
916+
}
917+
918+
UniValue RPCResult::MatchesType(const UniValue& result) const
919+
{
920+
if (m_skip_type_check) {
921+
return true;
922+
}
923+
924+
const auto exp_type = ExpectedType(m_type);
925+
if (!exp_type) return true; // can be any type, so nothing to check
926+
927+
if (*exp_type != result.getType()) {
928+
return strprintf("returned type is %s, but declared as %s in doc", uvTypeName(result.getType()), uvTypeName(*exp_type));
929+
}
930+
931+
if (UniValue::VARR == result.getType()) {
932+
UniValue errors(UniValue::VOBJ);
891933
for (size_t i{0}; i < result.get_array().size(); ++i) {
892934
// If there are more results than documented, re-use the last doc_inner.
893935
const RPCResult& doc_inner{m_inner.at(std::min(m_inner.size() - 1, i))};
894-
if (!doc_inner.MatchesType(result.get_array()[i])) return false;
936+
UniValue match{doc_inner.MatchesType(result.get_array()[i])};
937+
if (!match.isTrue()) errors.pushKV(strprintf("%d", i), match);
895938
}
896-
return true; // empty result array is valid
939+
if (errors.empty()) return true; // empty result array is valid
940+
return errors;
897941
}
898-
case Type::OBJ_DYN:
899-
case Type::OBJ: {
900-
if (UniValue::VOBJ != result.getType()) return false;
942+
943+
if (UniValue::VOBJ == result.getType()) {
901944
if (!m_inner.empty() && m_inner.at(0).m_type == Type::ELISION) return true;
945+
UniValue errors(UniValue::VOBJ);
902946
if (m_type == Type::OBJ_DYN) {
903947
const RPCResult& doc_inner{m_inner.at(0)}; // Assume all types are the same, randomly pick the first
904948
for (size_t i{0}; i < result.get_obj().size(); ++i) {
905-
if (!doc_inner.MatchesType(result.get_obj()[i])) {
906-
return false;
907-
}
949+
UniValue match{doc_inner.MatchesType(result.get_obj()[i])};
950+
if (!match.isTrue()) errors.pushKV(result.getKeys()[i], match);
908951
}
909-
return true; // empty result obj is valid
952+
if (errors.empty()) return true; // empty result obj is valid
953+
return errors;
910954
}
911955
std::set<std::string> doc_keys;
912956
for (const auto& doc_entry : m_inner) {
@@ -916,26 +960,26 @@ bool RPCResult::MatchesType(const UniValue& result) const
916960
result.getObjMap(result_obj);
917961
for (const auto& result_entry : result_obj) {
918962
if (doc_keys.find(result_entry.first) == doc_keys.end()) {
919-
return false; // missing documentation
963+
errors.pushKV(result_entry.first, "key returned that was not in doc");
920964
}
921965
}
922966

923967
for (const auto& doc_entry : m_inner) {
924968
const auto result_it{result_obj.find(doc_entry.m_key_name)};
925969
if (result_it == result_obj.end()) {
926970
if (!doc_entry.m_optional) {
927-
return false; // result is missing a required key
971+
errors.pushKV(doc_entry.m_key_name, "key missing, despite not being optional in doc");
928972
}
929973
continue;
930974
}
931-
if (!doc_entry.MatchesType(result_it->second)) {
932-
return false; // wrong type
933-
}
975+
UniValue match{doc_entry.MatchesType(result_it->second)};
976+
if (!match.isTrue()) errors.pushKV(doc_entry.m_key_name, match);
934977
}
935-
return true;
978+
if (errors.empty()) return true;
979+
return errors;
936980
}
937-
} // no default case, so the compiler can warn about missing cases
938-
NONFATAL_UNREACHABLE();
981+
982+
return true;
939983
}
940984

941985
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)