Skip to content

Commit bf1a1f1

Browse files
committed
rpc: Avoid returning HTTP errors for JSON-RPC 2.0 requests
Avoid returning HTTP status errors for non-batch JSON-RPC 2.0 requests if the RPC method failed but the HTTP request was otherwise valid. Batch requests already did not return HTTP errors previously.
1 parent 466b905 commit bf1a1f1

File tree

4 files changed

+31
-13
lines changed

4 files changed

+31
-13
lines changed

src/httprpc.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ static bool g_rpc_whitelist_default = false;
7575

7676
static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCRequest& jreq)
7777
{
78+
// Sending HTTP errors is a legacy JSON-RPC behavior.
79+
Assume(jreq.m_json_version != JSONRPCVersion::V2);
80+
7881
// Send error reply from json-rpc error object
7982
int nStatus = HTTP_INTERNAL_SERVER_ERROR;
8083
int code = objError.find_value("code").getInt<int>();
@@ -201,7 +204,12 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
201204
return false;
202205
}
203206

204-
reply = JSONRPCExec(jreq);
207+
// Legacy 1.0/1.1 behavior is for failed requests to throw
208+
// exceptions which return HTTP errors and RPC errors to the client.
209+
// 2.0 behavior is to catch exceptions and return HTTP success with
210+
// RPC errors, as long as there is not an actual HTTP server error.
211+
const bool catch_errors{jreq.m_json_version == JSONRPCVersion::V2};
212+
reply = JSONRPCExec(jreq, catch_errors);
205213

206214
// array of requests
207215
} else if (valRequest.isArray()) {
@@ -226,10 +234,11 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
226234
// Execute each request
227235
reply = UniValue::VARR;
228236
for (size_t i{0}; i < valRequest.size(); ++i) {
229-
// Batches include errors in the batch response, they do not throw
237+
// Batches never throw HTTP errors, they are always just included
238+
// in "HTTP OK" responses.
230239
try {
231240
jreq.parse(valRequest[i]);
232-
reply.push_back(JSONRPCExec(jreq));
241+
reply.push_back(JSONRPCExec(jreq, /*catch_errors=*/true));
233242
} catch (UniValue& e) {
234243
reply.push_back(JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version));
235244
} catch (const std::exception& e) {

src/rpc/server.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,20 @@ bool IsDeprecatedRPCEnabled(const std::string& method)
358358
return find(enabled_methods.begin(), enabled_methods.end(), method) != enabled_methods.end();
359359
}
360360

361-
UniValue JSONRPCExec(const JSONRPCRequest& jreq)
362-
{
363-
// Might throw exception. Single requests will throw and send HTTP error codes
364-
// but inside a batch, we just include the error object and return HTTP 200
365-
UniValue result = tableRPC.execute(jreq);
361+
UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors)
362+
{
363+
UniValue result;
364+
if (catch_errors) {
365+
try {
366+
result = tableRPC.execute(jreq);
367+
} catch (UniValue& e) {
368+
return JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
369+
} catch (const std::exception& e) {
370+
return JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_MISC_ERROR, e.what()), jreq.id, jreq.m_json_version);
371+
}
372+
} else {
373+
result = tableRPC.execute(jreq);
374+
}
366375

367376
return JSONRPCReplyObj(std::move(result), NullUniValue, jreq.id, jreq.m_json_version);
368377
}

src/rpc/server.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ extern CRPCTable tableRPC;
181181
void StartRPC();
182182
void InterruptRPC();
183183
void StopRPC();
184-
UniValue JSONRPCExec(const JSONRPCRequest& jreq);
184+
UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors);
185185

186186
// Drop witness when serializing for RPC?
187187
bool RPCSerializationWithoutWitness();

test/functional/interface_rpc.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ def test_http_status_codes(self):
188188
self.log.info("Testing HTTP status codes for JSON-RPC 2.0 requests...")
189189
# OK
190190
expect_http_rpc_status(200, None, self.nodes[0], "getblockhash", [0], 2, False)
191-
# RPC errors and HTTP errors
192-
expect_http_rpc_status(404, RPC_METHOD_NOT_FOUND, self.nodes[0], "invalidmethod", [], 2, False)
193-
expect_http_rpc_status(500, RPC_INVALID_PARAMETER, self.nodes[0], "getblockhash", [42], 2, False)
191+
# RPC errors but not HTTP errors
192+
expect_http_rpc_status(200, RPC_METHOD_NOT_FOUND, self.nodes[0], "invalidmethod", [], 2, False)
193+
expect_http_rpc_status(200, RPC_INVALID_PARAMETER, self.nodes[0], "getblockhash", [42], 2, False)
194194
# force-send invalidly formatted requests
195195
response, status = send_json_rpc(self.nodes[0], {"jsonrpc": 2, "method": "getblockcount"})
196196
assert_equal(response, {"id": None, "result": None, "error": {"code": RPC_INVALID_REQUEST, "message": "jsonrpc field must be a string"}})
@@ -212,7 +212,7 @@ def test_http_status_codes(self):
212212
expect_http_rpc_status(200, None, self.nodes[0], "generatetoaddress", [1, "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202"], 2, True)
213213
# The command worked even though there was no response
214214
assert_equal(block_count + 1, self.nodes[0].getblockcount())
215-
expect_http_rpc_status(500, RPC_INVALID_ADDRESS_OR_KEY, self.nodes[0], "generatetoaddress", [1, "invalid_address"], 2, True)
215+
expect_http_rpc_status(200, RPC_INVALID_ADDRESS_OR_KEY, self.nodes[0], "generatetoaddress", [1, "invalid_address"], 2, True)
216216
# Sanity check: command was not executed
217217
assert_equal(block_count + 1, self.nodes[0].getblockcount())
218218

0 commit comments

Comments
 (0)