// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once // Classifying a failed RPC call from structured data (curl code, request-sent bit, HTTP status, // JSON-RPC error code, non-JSON body) instead of substrings of what(). Header-only and pure, so the // test target can use it without compiling rpc_client.cpp. #include #include #include namespace dragonx { namespace rpc { enum class RpcFailureKind { None, // the call succeeded NotListening, // connection refused: nothing bound to the RPC port (not started, or already unbound) ConnectTimeout, // TCP connect didn't complete in time (Windows after the daemon unbinds; filtered port) ReplyTimeout, // request sent, no reply in time: the daemon holds it (busy, e.g. blocked on cs_main) QueueFull, // HTTP 500 "Work queue depth exceeded": rejected before any handler ran; holds no slot ShuttingDown, // HTTP 503: the daemon's HTTP server was interrupted for shutdown Unhandled, // HTTP 500 "Unhandled request": queued, then dropped by a shutting-down daemon Auth, // HTTP 401: wrong rpcuser/rpcpassword (or a stale .cookie) Warmup, // JSON-RPC -28 (RPC_IN_WARMUP): the node is still loading AppError, // the daemon ran the method and answered with a JSON-RPC error Aborted, // our own requestAbort() cut the transfer Deferred, // not sent: the node is busy (RpcBusyGate), so we held back rather than queue more Other, }; inline const char* rpcFailureKindName(RpcFailureKind kind) { switch (kind) { case RpcFailureKind::None: return "None"; case RpcFailureKind::NotListening: return "NotListening"; case RpcFailureKind::ConnectTimeout: return "ConnectTimeout"; case RpcFailureKind::ReplyTimeout: return "ReplyTimeout"; case RpcFailureKind::QueueFull: return "QueueFull"; case RpcFailureKind::ShuttingDown: return "ShuttingDown"; case RpcFailureKind::Unhandled: return "Unhandled"; case RpcFailureKind::Auth: return "Auth"; case RpcFailureKind::Warmup: return "Warmup"; case RpcFailureKind::AppError: return "AppError"; case RpcFailureKind::Aborted: return "Aborted"; case RpcFailureKind::Deferred: return "Deferred"; case RpcFailureKind::Other: return "Other"; } return "Other"; } // True for the two curl timeouts: what the old `what().find("Timeout")` checks meant. inline bool isRpcTimeout(RpcFailureKind kind) { return kind == RpcFailureKind::ConnectTimeout || kind == RpcFailureKind::ReplyTimeout; } /** * @param curlCode CURLcode of the transfer (CURLE_OK when a response arrived) * @param requestSent the request reached the daemon (CURLINFO_REQUEST_SIZE > 0). libcurl reports * connect- and reply-phase timeouts with the same code; this tells them apart. * @param httpStatus HTTP status, 0 when there was no response * @param rpcCode JSON-RPC error.code from a JSON error body, 0 when there was none * @param plainBody the response body when it was NOT JSON (dragonxd's HTTP-layer replies), else "" */ inline RpcFailureKind classifyRpcFailure(int curlCode, bool requestSent, long httpStatus, int rpcCode, const std::string& plainBody) { if (curlCode != CURLE_OK) { switch (curlCode) { case CURLE_ABORTED_BY_CALLBACK: return RpcFailureKind::Aborted; case CURLE_COULDNT_CONNECT: return RpcFailureKind::NotListening; case CURLE_OPERATION_TIMEDOUT: return requestSent ? RpcFailureKind::ReplyTimeout : RpcFailureKind::ConnectTimeout; default: return RpcFailureKind::Other; } } if (rpcCode == -28) return RpcFailureKind::Warmup; // takes precedence over the HTTP status if (rpcCode != 0) return RpcFailureKind::AppError; // 500, or 400/404 (invalid request / no method) if (httpStatus == 200) return RpcFailureKind::None; if (httpStatus == 401) return RpcFailureKind::Auth; if (httpStatus == 503) return RpcFailureKind::ShuttingDown; if (httpStatus == 500) { if (plainBody.find("Work queue depth exceeded") != std::string::npos) return RpcFailureKind::QueueFull; if (plainBody.find("Unhandled request") != std::string::npos) return RpcFailureKind::Unhandled; } return RpcFailureKind::Other; } /** * "RPC error: HTTP ", plus the body when the daemon sent a plain-text one (e.g. ": Work queue * depth exceeded"), trimmed and limited to 120 printable ASCII characters so an HTML page or binary * junk can't flood a log line. Exactly "RPC error: HTTP " when nothing printable remains. */ inline std::string formatHttpError(long httpStatus, const std::string& plainBody) { std::string text; bool pendingSpace = false; for (unsigned char c : plainBody) { if (text.size() >= 120) break; if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { pendingSpace = !text.empty(); } else if (c >= 0x21 && c <= 0x7e) { if (pendingSpace && text.size() < 119) text.push_back(' '); pendingSpace = false; text.push_back(static_cast(c)); } } std::string out = "RPC error: HTTP " + std::to_string(httpStatus); if (!text.empty()) out += ": " + text; return out; } /** * The daemon ran the method and answered with a JSON-RPC error. what() is the daemon's message; * `code` is its JSON-RPC error code, notably -28 (RPC_IN_WARMUP). Callers catch this type to mean * "the daemon said no" (e.g. no mnemonic -> fall back), as opposed to a transport failure. */ class RpcError : public std::runtime_error { public: RpcError(int errorCode, const std::string& message, long status = 0) : std::runtime_error(message), code(errorCode), httpStatus(status) {} RpcFailureKind kind() const { return code == -28 ? RpcFailureKind::Warmup : RpcFailureKind::AppError; } int code = 0; long httpStatus = 0; }; /** * The call produced no JSON-RPC answer: curl failed (refused, timed out, aborted), or the daemon's * HTTP layer turned the request away (401, 503, queue full, unhandled). what() keeps the historic * text ("RPC request failed: " / "RPC error: HTTP [: body]"). */ class RpcTransportError : public std::runtime_error { public: RpcTransportError(RpcFailureKind failureKind, const std::string& message, int curl = CURLE_OK, long status = 0, bool sent = false) : std::runtime_error(message), kind(failureKind), curlCode(curl), httpStatus(status), requestSent(sent) {} RpcFailureKind kind = RpcFailureKind::Other; int curlCode = CURLE_OK; long httpStatus = 0; bool requestSent = false; }; // The kind of any exception an RPC call threw (Other for anything that isn't an RPC error). inline RpcFailureKind rpcFailureKindOf(const std::exception& e) { if (const auto* t = dynamic_cast(&e)) return t->kind; if (const auto* r = dynamic_cast(&e)) return r->kind(); return RpcFailureKind::Other; } // What a failed walletpassphrase call means. Only WrongPassphrase (-14) may count toward the lockout; // a busy/warming-up node, a wallet that is not encrypted, or anything unknown is never a bad guess. enum class UnlockFailure { WrongPassphrase, NodeBusy, NotEncrypted, Other }; inline UnlockFailure classifyUnlockFailure(const std::exception& e) { if (const auto* r = dynamic_cast(&e)) { if (r->code == -14) return UnlockFailure::WrongPassphrase; // RPC_WALLET_PASSPHRASE_INCORRECT if (r->code == -15) return UnlockFailure::NotEncrypted; // RPC_WALLET_WRONG_ENC_STATE if (r->code == -28) return UnlockFailure::NodeBusy; // RPC_IN_WARMUP return UnlockFailure::Other; } switch (rpcFailureKindOf(e)) { case RpcFailureKind::ReplyTimeout: case RpcFailureKind::QueueFull: case RpcFailureKind::Deferred: case RpcFailureKind::Warmup: case RpcFailureKind::ConnectTimeout: case RpcFailureKind::NotListening: return UnlockFailure::NodeBusy; default: return UnlockFailure::Other; } } } // namespace rpc } // namespace dragonx