feat(rpc): classify RPC failures from structured data, not message text
Every caller guessed what went wrong from what(): "Timeout" meant busy, "401" meant auth, and warmup was code -28 or one of six substrings. A non-JSON HTTP 500 lost its body, so "Work queue depth exceeded" read as a bare "HTTP 500", and libcurl reports connect- and reply-phase timeouts with the same code. New header-only src/rpc/rpc_failure.h: - RpcFailureKind (NotListening, ConnectTimeout, ReplyTimeout, QueueFull, ShuttingDown, Unhandled, Auth, Warmup, AppError, Aborted, Other) and a pure classifyRpcFailure(curl code, request-sent, HTTP status, JSON-RPC code, plain-text body) - formatHttpError keeps a short, printable plain-text body - RpcError (moved here) stays "the daemon answered with a JSON-RPC error". The new RpcTransportError means "no JSON-RPC answer": curl failures and HTTP-layer refusals (401, 503, queue full, unhandled). Keeping them separate matters: callers catch RpcError to mean "the daemon said no" (chat identity's no-mnemonic fallback, seed export), so a timeout must not be one. They now treat a queue-full 500 as transient rather than as "no". RPCClient: - performCall tells the two timeouts apart with CURLINFO_REQUEST_SIZE (0 = never reached the daemon; reset on every transfer, checked on libcurl 7.81), and callRaw now shares performCall and the HTTP-failure path. - connect() decides warmup by the -28 code only, so a plain-text body saying "Loading" can't pass for warmup, and records getLastConnectFailureKind(). - "Connection failed" names the kind and is logged once, then every 100th repeat (one incident logged it 60k times). - secret-bearing calls never echo a response body. tryConnect's "401"/"Timeout" checks now use the kind, and CoreRefreshResult carries blockchainErrKind/balanceErrKind for the busy handling to come. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
-15
@@ -5,6 +5,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
#include "rpc_failure.h" // RpcError, RpcTransportError, RpcFailureKind
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
@@ -20,21 +21,6 @@ using json = nlohmann::json;
|
||||
using Callback = std::function<void(const json&)>;
|
||||
using ErrorCallback = std::function<void(const std::string&)>;
|
||||
|
||||
/**
|
||||
* @brief A JSON-RPC error carrying the daemon's numeric error code.
|
||||
*
|
||||
* what() preserves the exact human-readable message (so existing string matching
|
||||
* still works); `code` exposes the JSON-RPC error code — notably -28 (RPC_IN_WARMUP)
|
||||
* for a daemon still starting up. Derives from std::runtime_error, so every existing
|
||||
* `catch (const std::exception&)` continues to handle it unchanged.
|
||||
*/
|
||||
class RpcError : public std::runtime_error {
|
||||
public:
|
||||
RpcError(int errorCode, const std::string& message)
|
||||
: std::runtime_error(message), code(errorCode) {}
|
||||
int code = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief JSON-RPC client for dragonxd
|
||||
*
|
||||
@@ -117,6 +103,8 @@ public:
|
||||
* @brief Get the error message from the last failed connect() attempt.
|
||||
*/
|
||||
const std::string& getLastConnectError() const { return last_connect_error_; }
|
||||
/** @brief How the last connect() probe went: None on success, Warmup for a warming-up node, else the failure. */
|
||||
RpcFailureKind getLastConnectFailureKind() const { return last_connect_kind_; }
|
||||
json getLastConnectInfo() const;
|
||||
|
||||
static void setTraceCallback(TraceCallback callback);
|
||||
@@ -274,6 +262,10 @@ private:
|
||||
// parse tree's string values are zeroed before it is discarded — parseRpcResult returns a COPY of
|
||||
// the result node, so its own tree would otherwise free the secret un-wiped (B7).
|
||||
static json parseRpcResult(long httpCode, const std::string& body, bool scrubSource = false);
|
||||
// A non-200 reply: RpcError when the body carries a JSON-RPC error, else RpcTransportError classified
|
||||
// from the status and plain-text body. hideBody keeps the body out of the message (secret calls).
|
||||
[[noreturn]] static void throwHttpFailure(long httpCode, const std::string& body, bool hideBody);
|
||||
void logConnectFailure(const std::string& msg);
|
||||
|
||||
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
|
||||
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
|
||||
@@ -286,6 +278,9 @@ private:
|
||||
bool warming_up_ = false;
|
||||
std::string warmup_status_;
|
||||
std::string last_connect_error_;
|
||||
RpcFailureKind last_connect_kind_ = RpcFailureKind::None;
|
||||
std::string logged_connect_error_; // last "Connection failed" text logged (rate limit)
|
||||
unsigned connect_error_repeats_ = 0; // identical failures since it was logged
|
||||
json last_connect_info_;
|
||||
mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access
|
||||
|
||||
|
||||
Reference in New Issue
Block a user