mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-10 10:52:31 -05:00
RPC: make RPCResult::MatchesType return useful errors
This commit is contained in:
parent
f4ef856375
commit
3d1a4d8a45
2 changed files with 79 additions and 33 deletions
106
src/rpc/util.cpp
106
src/rpc/util.cpp
|
@ -2,6 +2,7 @@
|
||||||
// Distributed under the MIT software license, see the accompanying
|
// Distributed under the MIT software license, see the accompanying
|
||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
#include <clientversion.h>
|
||||||
#include <consensus/amount.h>
|
#include <consensus/amount.h>
|
||||||
#include <key_io.h>
|
#include <key_io.h>
|
||||||
#include <outputtype.h>
|
#include <outputtype.h>
|
||||||
|
@ -581,7 +582,26 @@ UniValue RPCHelpMan::HandleRequest(const JSONRPCRequest& request) const
|
||||||
}
|
}
|
||||||
UniValue ret = m_fun(*this, request);
|
UniValue ret = m_fun(*this, request);
|
||||||
if (gArgs.GetBoolArg("-rpcdoccheck", DEFAULT_RPC_DOC_CHECK)) {
|
if (gArgs.GetBoolArg("-rpcdoccheck", DEFAULT_RPC_DOC_CHECK)) {
|
||||||
CHECK_NONFATAL(std::any_of(m_results.m_results.begin(), m_results.m_results.end(), [&ret](const RPCResult& res) { return res.MatchesType(ret); }));
|
UniValue mismatch{UniValue::VARR};
|
||||||
|
for (const auto& res : m_results.m_results) {
|
||||||
|
UniValue match{res.MatchesType(ret)};
|
||||||
|
if (match.isTrue()) {
|
||||||
|
mismatch.setNull();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mismatch.push_back(match);
|
||||||
|
}
|
||||||
|
if (!mismatch.isNull()) {
|
||||||
|
std::string explain{
|
||||||
|
mismatch.empty() ? "no possible results defined" :
|
||||||
|
mismatch.size() == 1 ? mismatch[0].write(4) :
|
||||||
|
mismatch.write(4)};
|
||||||
|
throw std::runtime_error{
|
||||||
|
strprintf("Internal bug detected: RPC call \"%s\" returned incorrect type:\n%s\n%s %s\nPlease report this issue here: %s\n",
|
||||||
|
m_name, explain,
|
||||||
|
PACKAGE_NAME, FormatFullVersion(),
|
||||||
|
PACKAGE_BUGREPORT)};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -860,53 +880,77 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const
|
||||||
NONFATAL_UNREACHABLE();
|
NONFATAL_UNREACHABLE();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RPCResult::MatchesType(const UniValue& result) const
|
static const std::optional<UniValue::VType> ExpectedType(RPCResult::Type type)
|
||||||
{
|
{
|
||||||
if (m_skip_type_check) {
|
using Type = RPCResult::Type;
|
||||||
return true;
|
switch (type) {
|
||||||
}
|
|
||||||
switch (m_type) {
|
|
||||||
case Type::ELISION:
|
case Type::ELISION:
|
||||||
case Type::ANY: {
|
case Type::ANY: {
|
||||||
return true;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
case Type::NONE: {
|
case Type::NONE: {
|
||||||
return UniValue::VNULL == result.getType();
|
return UniValue::VNULL;
|
||||||
}
|
}
|
||||||
case Type::STR:
|
case Type::STR:
|
||||||
case Type::STR_HEX: {
|
case Type::STR_HEX: {
|
||||||
return UniValue::VSTR == result.getType();
|
return UniValue::VSTR;
|
||||||
}
|
}
|
||||||
case Type::NUM:
|
case Type::NUM:
|
||||||
case Type::STR_AMOUNT:
|
case Type::STR_AMOUNT:
|
||||||
case Type::NUM_TIME: {
|
case Type::NUM_TIME: {
|
||||||
return UniValue::VNUM == result.getType();
|
return UniValue::VNUM;
|
||||||
}
|
}
|
||||||
case Type::BOOL: {
|
case Type::BOOL: {
|
||||||
return UniValue::VBOOL == result.getType();
|
return UniValue::VBOOL;
|
||||||
}
|
}
|
||||||
case Type::ARR_FIXED:
|
case Type::ARR_FIXED:
|
||||||
case Type::ARR: {
|
case Type::ARR: {
|
||||||
if (UniValue::VARR != result.getType()) return false;
|
return UniValue::VARR;
|
||||||
for (size_t i{0}; i < result.get_array().size(); ++i) {
|
|
||||||
// If there are more results than documented, re-use the last doc_inner.
|
|
||||||
const RPCResult& doc_inner{m_inner.at(std::min(m_inner.size() - 1, i))};
|
|
||||||
if (!doc_inner.MatchesType(result.get_array()[i])) return false;
|
|
||||||
}
|
|
||||||
return true; // empty result array is valid
|
|
||||||
}
|
}
|
||||||
case Type::OBJ_DYN:
|
case Type::OBJ_DYN:
|
||||||
case Type::OBJ: {
|
case Type::OBJ: {
|
||||||
if (UniValue::VOBJ != result.getType()) return false;
|
return UniValue::VOBJ;
|
||||||
|
}
|
||||||
|
} // no default case, so the compiler can warn about missing cases
|
||||||
|
NONFATAL_UNREACHABLE();
|
||||||
|
}
|
||||||
|
|
||||||
|
UniValue RPCResult::MatchesType(const UniValue& result) const
|
||||||
|
{
|
||||||
|
if (m_skip_type_check) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto exp_type = ExpectedType(m_type);
|
||||||
|
if (!exp_type) return true; // can be any type, so nothing to check
|
||||||
|
|
||||||
|
if (*exp_type != result.getType()) {
|
||||||
|
return strprintf("returned type is %s, but declared as %s in doc", uvTypeName(result.getType()), uvTypeName(*exp_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UniValue::VARR == result.getType()) {
|
||||||
|
UniValue errors(UniValue::VOBJ);
|
||||||
|
for (size_t i{0}; i < result.get_array().size(); ++i) {
|
||||||
|
// If there are more results than documented, re-use the last doc_inner.
|
||||||
|
const RPCResult& doc_inner{m_inner.at(std::min(m_inner.size() - 1, i))};
|
||||||
|
UniValue match{doc_inner.MatchesType(result.get_array()[i])};
|
||||||
|
if (!match.isTrue()) errors.pushKV(strprintf("%d", i), match);
|
||||||
|
}
|
||||||
|
if (errors.empty()) return true; // empty result array is valid
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UniValue::VOBJ == result.getType()) {
|
||||||
if (!m_inner.empty() && m_inner.at(0).m_type == Type::ELISION) return true;
|
if (!m_inner.empty() && m_inner.at(0).m_type == Type::ELISION) return true;
|
||||||
|
UniValue errors(UniValue::VOBJ);
|
||||||
if (m_type == Type::OBJ_DYN) {
|
if (m_type == Type::OBJ_DYN) {
|
||||||
const RPCResult& doc_inner{m_inner.at(0)}; // Assume all types are the same, randomly pick the first
|
const RPCResult& doc_inner{m_inner.at(0)}; // Assume all types are the same, randomly pick the first
|
||||||
for (size_t i{0}; i < result.get_obj().size(); ++i) {
|
for (size_t i{0}; i < result.get_obj().size(); ++i) {
|
||||||
if (!doc_inner.MatchesType(result.get_obj()[i])) {
|
UniValue match{doc_inner.MatchesType(result.get_obj()[i])};
|
||||||
return false;
|
if (!match.isTrue()) errors.pushKV(result.getKeys()[i], match);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true; // empty result obj is valid
|
if (errors.empty()) return true; // empty result obj is valid
|
||||||
|
return errors;
|
||||||
}
|
}
|
||||||
std::set<std::string> doc_keys;
|
std::set<std::string> doc_keys;
|
||||||
for (const auto& doc_entry : m_inner) {
|
for (const auto& doc_entry : m_inner) {
|
||||||
|
@ -916,7 +960,7 @@ bool RPCResult::MatchesType(const UniValue& result) const
|
||||||
result.getObjMap(result_obj);
|
result.getObjMap(result_obj);
|
||||||
for (const auto& result_entry : result_obj) {
|
for (const auto& result_entry : result_obj) {
|
||||||
if (doc_keys.find(result_entry.first) == doc_keys.end()) {
|
if (doc_keys.find(result_entry.first) == doc_keys.end()) {
|
||||||
return false; // missing documentation
|
errors.pushKV(result_entry.first, "key returned that was not in doc");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -924,18 +968,18 @@ bool RPCResult::MatchesType(const UniValue& result) const
|
||||||
const auto result_it{result_obj.find(doc_entry.m_key_name)};
|
const auto result_it{result_obj.find(doc_entry.m_key_name)};
|
||||||
if (result_it == result_obj.end()) {
|
if (result_it == result_obj.end()) {
|
||||||
if (!doc_entry.m_optional) {
|
if (!doc_entry.m_optional) {
|
||||||
return false; // result is missing a required key
|
errors.pushKV(doc_entry.m_key_name, "key missing, despite not being optional in doc");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!doc_entry.MatchesType(result_it->second)) {
|
UniValue match{doc_entry.MatchesType(result_it->second)};
|
||||||
return false; // wrong type
|
if (!match.isTrue()) errors.pushKV(doc_entry.m_key_name, match);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
if (errors.empty()) return true;
|
||||||
|
return errors;
|
||||||
}
|
}
|
||||||
} // no default case, so the compiler can warn about missing cases
|
|
||||||
NONFATAL_UNREACHABLE();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCResult::CheckInnerDoc() const
|
void RPCResult::CheckInnerDoc() const
|
||||||
|
|
|
@ -324,8 +324,10 @@ struct RPCResult {
|
||||||
std::string ToStringObj() const;
|
std::string ToStringObj() const;
|
||||||
/** Return the description string, including the result type. */
|
/** Return the description string, including the result type. */
|
||||||
std::string ToDescriptionString() const;
|
std::string ToDescriptionString() const;
|
||||||
/** Check whether the result JSON type matches. */
|
/** Check whether the result JSON type matches.
|
||||||
bool MatchesType(const UniValue& result) const;
|
* Returns true if type matches, or object describing error(s) if not.
|
||||||
|
*/
|
||||||
|
UniValue MatchesType(const UniValue& result) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void CheckInnerDoc() const;
|
void CheckInnerDoc() const;
|
||||||
|
|
Loading…
Add table
Reference in a new issue