Skip to content

Commit 75118a6

Browse files
committed
Merge bitcoin/bitcoin#27101: Support JSON-RPC 2.0 when requested by client
cbc6c44 doc: add comments and release-notes for JSON-RPC 2.0 (Matthew Zipkin) e7ee80d rpc: JSON-RPC 2.0 should not respond to "notifications" (Matthew Zipkin) bf1a1f1 rpc: Avoid returning HTTP errors for JSON-RPC 2.0 requests (Matthew Zipkin) 466b905 rpc: Add "jsonrpc" field and drop null "result"/"error" fields (Matthew Zipkin) 2ca1460 rpc: identify JSON-RPC 2.0 requests (Matthew Zipkin) a64a2b7 rpc: refactor single/batch requests (Matthew Zipkin) df6e375 rpc: Avoid copies in JSONRPCReplyObj() (Matthew Zipkin) 09416f9 test: cover JSONRPC 2.0 requests, batches, and notifications (Matthew Zipkin) 4202c17 test: refactor interface_rpc.py (Matthew Zipkin) Pull request description: Closes bitcoin/bitcoin#2960 Bitcoin Core's JSONRPC server behaves with a special blend of 1.0, 1.1 and 2.0 behaviors. This introduces compliance issues with more strict clients. There are the major misbehaviors that I found: - returning non-200 HTTP codes for RPC errors like "Method not found" (this is not a server error or an HTTP error) - returning both `"error"` and `"result"` fields together in a response object. - different error-handling behavior for single and batched RPC requests (batches contain errors in the response but single requests will actually throw HTTP errors) bitcoin/bitcoin#15495 added regression tests after a discussion in bitcoin/bitcoin#15381 to kinda lock in our RPC behavior to preserve backwards compatibility. bitcoin/bitcoin#12435 was an attempt to allow strict 2.0 compliance behind a flag, but was abandoned. The approach in this PR is not strict and preserves backwards compatibility in a familiar bitcoin-y way: all old behavior is preserved, but new rules are applied to clients that opt in. One of the rules in the [JSON RPC 2.0 spec](https://www.jsonrpc.org/specification#request_object) is that the kv pair `"jsonrpc": "2.0"` must be present in the request. Well, let's just use that to trigger strict 2.0 behavior! When that kv pair is included in a request object, the [response will adhere to strict JSON-RPC 2.0 rules](https://www.jsonrpc.org/specification#response_object), essentially: - always return HTTP 200 "OK" unless there really is a server error or malformed request - either return `"error"` OR `"result"` but never both - same behavior for single and batch requests If this is merged next steps can be: - Refactor bitcoin-cli to always use strict 2.0 - Refactor the python test framework to always use strict 2.0 for everything - Begin deprecation process for 1.0/1.1 behavior (?) If we can one day remove the old 1.0/1.1 behavior we can clean up the rpc code quite a bit. ACKs for top commit: cbergqvist: re ACK cbc6c44 ryanofsky: Code review ACK cbc6c44. Just suggested changes since the last review: changing uncaught exception error code from PARSE_ERROR to MISC_ERROR, renaming a few things, and adding comments. tdb3: re ACK for cbc6c44 Tree-SHA512: 0b702ed32368b34b29ad570d090951a7aeb56e3b0f2baf745bd32fdc58ef68fee6b0b8fad901f1ca42573ed714b150303829cddad4a34ca7ad847350feeedb36
2 parents dd42a5d + cbc6c44 commit 75118a6

File tree

13 files changed

+355
-101
lines changed

13 files changed

+355
-101
lines changed

doc/JSON-RPC-interface.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,22 @@ major version via the `-deprecatedrpc=` command line option. The release notes
7474
of a new major release come with detailed instructions on what RPC features
7575
were deprecated and how to re-enable them temporarily.
7676

77+
## JSON-RPC 1.1 vs 2.0
78+
79+
The server recognizes [JSON-RPC v2.0](https://www.jsonrpc.org/specification) requests
80+
and responds accordingly. A 2.0 request is identified by the presence of
81+
`"jsonrpc": "2.0"` in the request body. If that key + value is not present in a request,
82+
the legacy JSON-RPC v1.1 protocol is followed instead, which was the only available
83+
protocol in previous releases.
84+
85+
|| 1.1 | 2.0 |
86+
|-|-|-|
87+
| Request marker | `"version": "1.1"` (or none) | `"jsonrpc": "2.0"` |
88+
| Response marker | (none) | `"jsonrpc": "2.0"` |
89+
| `"error"` and `"result"` fields in response | both present | only one is present |
90+
| HTTP codes in response | `200` unless there is any kind of RPC error (invalid parameters, method not found, etc) | Always `200` unless there is an actual HTTP server error (request parsing error, endpoint not found, etc) |
91+
| Notifications: requests that get no reply | (not supported) | Supported for requests that exclude the "id" field |
92+
7793
## Security
7894

7995
The RPC interface allows other programs to control Bitcoin Core,

doc/release-notes-27101.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
JSON-RPC
2+
--------
3+
4+
The JSON-RPC server now recognizes JSON-RPC 2.0 requests and responds with
5+
strict adherence to the specification (https://www.jsonrpc.org/specification):
6+
7+
- Returning HTTP "204 No Content" responses to JSON-RPC 2.0 notifications instead of full responses.
8+
- Returning HTTP "200 OK" responses in all other cases, rather than 404 responses for unknown methods, 500 responses for invalid parameters, etc.
9+
- Returning either "result" fields or "error" fields in JSON-RPC responses, rather than returning both fields with one field set to null.

src/bitcoin-cli.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ class AddrinfoRequestHandler : public BaseRequestHandler
298298
}
299299
addresses.pushKV("total", total);
300300
result.pushKV("addresses_known", addresses);
301-
return JSONRPCReplyObj(result, NullUniValue, 1);
301+
return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
302302
}
303303
};
304304

@@ -367,7 +367,7 @@ class GetinfoRequestHandler: public BaseRequestHandler
367367
}
368368
result.pushKV("relayfee", batch[ID_NETWORKINFO]["result"]["relayfee"]);
369369
result.pushKV("warnings", batch[ID_NETWORKINFO]["result"]["warnings"]);
370-
return JSONRPCReplyObj(result, NullUniValue, 1);
370+
return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
371371
}
372372
};
373373

@@ -622,7 +622,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
622622
}
623623
}
624624

625-
return JSONRPCReplyObj(UniValue{result}, NullUniValue, 1);
625+
return JSONRPCReplyObj(UniValue{result}, NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
626626
}
627627

628628
const std::string m_help_doc{
@@ -709,7 +709,7 @@ class GenerateToAddressRequestHandler : public BaseRequestHandler
709709
UniValue result(UniValue::VOBJ);
710710
result.pushKV("address", address_str);
711711
result.pushKV("blocks", reply.get_obj()["result"]);
712-
return JSONRPCReplyObj(result, NullUniValue, 1);
712+
return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
713713
}
714714
protected:
715715
std::string address_str;

src/httprpc.cpp

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ static std::vector<std::vector<std::string>> g_rpcauth;
7373
static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
7474
static bool g_rpc_whitelist_default = false;
7575

76-
static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const UniValue& id)
76+
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>();
@@ -84,7 +87,7 @@ static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const Uni
8487
else if (code == RPC_METHOD_NOT_FOUND)
8588
nStatus = HTTP_NOT_FOUND;
8689

87-
std::string strReply = JSONRPCReply(NullUniValue, objError, id);
90+
std::string strReply = JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version).write() + "\n";
8891

8992
req->WriteHeader("Content-Type", "application/json");
9093
req->WriteReply(nStatus, strReply);
@@ -185,7 +188,7 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
185188
// Set the URI
186189
jreq.URI = req->GetURI();
187190

188-
std::string strReply;
191+
UniValue reply;
189192
bool user_has_whitelist = g_rpc_whitelist.count(jreq.authUser);
190193
if (!user_has_whitelist && g_rpc_whitelist_default) {
191194
LogPrintf("RPC User %s not allowed to call any methods\n", jreq.authUser);
@@ -200,13 +203,23 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
200203
req->WriteReply(HTTP_FORBIDDEN);
201204
return false;
202205
}
203-
UniValue result = tableRPC.execute(jreq);
204206

205-
// Send reply
206-
strReply = JSONRPCReply(result, NullUniValue, jreq.id);
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);
213+
214+
if (jreq.IsNotification()) {
215+
// Even though we do execute notifications, we do not respond to them
216+
req->WriteReply(HTTP_NO_CONTENT);
217+
return true;
218+
}
207219

208220
// array of requests
209221
} else if (valRequest.isArray()) {
222+
// Check authorization for each request's method
210223
if (user_has_whitelist) {
211224
for (unsigned int reqIdx = 0; reqIdx < valRequest.size(); reqIdx++) {
212225
if (!valRequest[reqIdx].isObject()) {
@@ -223,18 +236,49 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
223236
}
224237
}
225238
}
226-
strReply = JSONRPCExecBatch(jreq, valRequest.get_array());
239+
240+
// Execute each request
241+
reply = UniValue::VARR;
242+
for (size_t i{0}; i < valRequest.size(); ++i) {
243+
// Batches never throw HTTP errors, they are always just included
244+
// in "HTTP OK" responses. Notifications never get any response.
245+
UniValue response;
246+
try {
247+
jreq.parse(valRequest[i]);
248+
response = JSONRPCExec(jreq, /*catch_errors=*/true);
249+
} catch (UniValue& e) {
250+
response = JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
251+
} catch (const std::exception& e) {
252+
response = JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version);
253+
}
254+
if (!jreq.IsNotification()) {
255+
reply.push_back(std::move(response));
256+
}
257+
}
258+
// Return no response for an all-notification batch, but only if the
259+
// batch request is non-empty. Technically according to the JSON-RPC
260+
// 2.0 spec, an empty batch request should also return no response,
261+
// However, if the batch request is empty, it means the request did
262+
// not contain any JSON-RPC version numbers, so returning an empty
263+
// response could break backwards compatibility with old RPC clients
264+
// relying on previous behavior. Return an empty array instead of an
265+
// empty response in this case to favor being backwards compatible
266+
// over complying with the JSON-RPC 2.0 spec in this case.
267+
if (reply.size() == 0 && valRequest.size() > 0) {
268+
req->WriteReply(HTTP_NO_CONTENT);
269+
return true;
270+
}
227271
}
228272
else
229273
throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
230274

231275
req->WriteHeader("Content-Type", "application/json");
232-
req->WriteReply(HTTP_OK, strReply);
233-
} catch (const UniValue& objError) {
234-
JSONErrorReply(req, objError, jreq.id);
276+
req->WriteReply(HTTP_OK, reply.write() + "\n");
277+
} catch (UniValue& e) {
278+
JSONErrorReply(req, std::move(e), jreq);
235279
return false;
236280
} catch (const std::exception& e) {
237-
JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
281+
JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq);
238282
return false;
239283
}
240284
return true;

src/rpc/protocol.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
enum HTTPStatusCode
1111
{
1212
HTTP_OK = 200,
13+
HTTP_NO_CONTENT = 204,
1314
HTTP_BAD_REQUEST = 400,
1415
HTTP_UNAUTHORIZED = 401,
1516
HTTP_FORBIDDEN = 403,

src/rpc/request.cpp

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@
2626
*
2727
* 1.0 spec: http://json-rpc.org/wiki/specification
2828
* 1.2 spec: http://jsonrpc.org/historical/json-rpc-over-http.html
29+
*
30+
* If the server receives a request with the JSON-RPC 2.0 marker `{"jsonrpc": "2.0"}`
31+
* then Bitcoin will respond with a strictly specified response.
32+
* It will only return an HTTP error code if an actual HTTP error is encountered
33+
* such as the endpoint is not found (404) or the request is not formatted correctly (500).
34+
* Otherwise the HTTP code is always OK (200) and RPC errors will be included in the
35+
* response body.
36+
*
37+
* 2.0 spec: https://www.jsonrpc.org/specification
38+
*
39+
* Also see http://www.simple-is-better.org/rpc/#differences-between-1-0-and-2-0
2940
*/
3041

3142
UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id)
@@ -37,24 +48,25 @@ UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params,
3748
return request;
3849
}
3950

40-
UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id)
51+
UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue> id, JSONRPCVersion jsonrpc_version)
4152
{
4253
UniValue reply(UniValue::VOBJ);
43-
if (!error.isNull())
44-
reply.pushKV("result", NullUniValue);
45-
else
46-
reply.pushKV("result", result);
47-
reply.pushKV("error", error);
48-
reply.pushKV("id", id);
54+
// Add JSON-RPC version number field in v2 only.
55+
if (jsonrpc_version == JSONRPCVersion::V2) reply.pushKV("jsonrpc", "2.0");
56+
57+
// Add both result and error fields in v1, even though one will be null.
58+
// Omit the null field in v2.
59+
if (error.isNull()) {
60+
reply.pushKV("result", std::move(result));
61+
if (jsonrpc_version == JSONRPCVersion::V1_LEGACY) reply.pushKV("error", NullUniValue);
62+
} else {
63+
if (jsonrpc_version == JSONRPCVersion::V1_LEGACY) reply.pushKV("result", NullUniValue);
64+
reply.pushKV("error", std::move(error));
65+
}
66+
if (id.has_value()) reply.pushKV("id", std::move(id.value()));
4967
return reply;
5068
}
5169

52-
std::string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id)
53-
{
54-
UniValue reply = JSONRPCReplyObj(result, error, id);
55-
return reply.write() + "\n";
56-
}
57-
5870
UniValue JSONRPCError(int code, const std::string& message)
5971
{
6072
UniValue error(UniValue::VOBJ);
@@ -171,7 +183,30 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
171183
const UniValue& request = valRequest.get_obj();
172184

173185
// Parse id now so errors from here on will have the id
174-
id = request.find_value("id");
186+
if (request.exists("id")) {
187+
id = request.find_value("id");
188+
} else {
189+
id = std::nullopt;
190+
}
191+
192+
// Check for JSON-RPC 2.0 (default 1.1)
193+
m_json_version = JSONRPCVersion::V1_LEGACY;
194+
const UniValue& jsonrpc_version = request.find_value("jsonrpc");
195+
if (!jsonrpc_version.isNull()) {
196+
if (!jsonrpc_version.isStr()) {
197+
throw JSONRPCError(RPC_INVALID_REQUEST, "jsonrpc field must be a string");
198+
}
199+
// The "jsonrpc" key was added in the 2.0 spec, but some older documentation
200+
// incorrectly included {"jsonrpc":"1.0"} in a request object, so we
201+
// maintain that for backwards compatibility.
202+
if (jsonrpc_version.get_str() == "1.0") {
203+
m_json_version = JSONRPCVersion::V1_LEGACY;
204+
} else if (jsonrpc_version.get_str() == "2.0") {
205+
m_json_version = JSONRPCVersion::V2;
206+
} else {
207+
throw JSONRPCError(RPC_INVALID_REQUEST, "JSON-RPC version not supported");
208+
}
209+
}
175210

176211
// Parse method
177212
const UniValue& valMethod{request.find_value("method")};

src/rpc/request.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
#define BITCOIN_RPC_REQUEST_H
88

99
#include <any>
10+
#include <optional>
1011
#include <string>
1112

1213
#include <univalue.h>
1314

15+
enum class JSONRPCVersion {
16+
V1_LEGACY,
17+
V2
18+
};
19+
1420
UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id);
15-
UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id);
16-
std::string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id);
21+
UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue> id, JSONRPCVersion jsonrpc_version);
1722
UniValue JSONRPCError(int code, const std::string& message);
1823

1924
/** Generate a new RPC authentication cookie and write it to disk */
@@ -28,16 +33,18 @@ std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue& in);
2833
class JSONRPCRequest
2934
{
3035
public:
31-
UniValue id;
36+
std::optional<UniValue> id = UniValue::VNULL;
3237
std::string strMethod;
3338
UniValue params;
3439
enum Mode { EXECUTE, GET_HELP, GET_ARGS } mode = EXECUTE;
3540
std::string URI;
3641
std::string authUser;
3742
std::string peerAddr;
3843
std::any context;
44+
JSONRPCVersion m_json_version = JSONRPCVersion::V1_LEGACY;
3945

4046
void parse(const UniValue& valRequest);
47+
[[nodiscard]] bool IsNotification() const { return !id.has_value() && m_json_version == JSONRPCVersion::V2; };
4148
};
4249

4350
#endif // BITCOIN_RPC_REQUEST_H

src/rpc/server.cpp

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -360,36 +360,22 @@ bool IsDeprecatedRPCEnabled(const std::string& method)
360360
return find(enabled_methods.begin(), enabled_methods.end(), method) != enabled_methods.end();
361361
}
362362

363-
static UniValue JSONRPCExecOne(JSONRPCRequest jreq, const UniValue& req)
364-
{
365-
UniValue rpc_result(UniValue::VOBJ);
366-
367-
try {
368-
jreq.parse(req);
369-
370-
UniValue result = tableRPC.execute(jreq);
371-
rpc_result = JSONRPCReplyObj(result, NullUniValue, jreq.id);
372-
}
373-
catch (const UniValue& objError)
374-
{
375-
rpc_result = JSONRPCReplyObj(NullUniValue, objError, jreq.id);
376-
}
377-
catch (const std::exception& e)
378-
{
379-
rpc_result = JSONRPCReplyObj(NullUniValue,
380-
JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
363+
UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors)
364+
{
365+
UniValue result;
366+
if (catch_errors) {
367+
try {
368+
result = tableRPC.execute(jreq);
369+
} catch (UniValue& e) {
370+
return JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
371+
} catch (const std::exception& e) {
372+
return JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_MISC_ERROR, e.what()), jreq.id, jreq.m_json_version);
373+
}
374+
} else {
375+
result = tableRPC.execute(jreq);
381376
}
382377

383-
return rpc_result;
384-
}
385-
386-
std::string JSONRPCExecBatch(const JSONRPCRequest& jreq, const UniValue& vReq)
387-
{
388-
UniValue ret(UniValue::VARR);
389-
for (unsigned int reqIdx = 0; reqIdx < vReq.size(); reqIdx++)
390-
ret.push_back(JSONRPCExecOne(jreq, vReq[reqIdx]));
391-
392-
return ret.write() + "\n";
378+
return JSONRPCReplyObj(std::move(result), NullUniValue, jreq.id, jreq.m_json_version);
393379
}
394380

395381
/**

src/rpc/server.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,6 @@ extern CRPCTable tableRPC;
179179
void StartRPC();
180180
void InterruptRPC();
181181
void StopRPC();
182-
std::string JSONRPCExecBatch(const JSONRPCRequest& jreq, const UniValue& vReq);
182+
UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors);
183183

184184
#endif // BITCOIN_RPC_SERVER_H

src/rpc/util.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ std::string HelpExampleCliNamed(const std::string& methodname, const RPCArgList&
175175

176176
std::string HelpExampleRpc(const std::string& methodname, const std::string& args)
177177
{
178-
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"1.0\", \"id\": \"curltest\", "
178+
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"2.0\", \"id\": \"curltest\", "
179179
"\"method\": \"" + methodname + "\", \"params\": [" + args + "]}' -H 'content-type: text/plain;' http://127.0.0.1:8332/\n";
180180
}
181181

@@ -186,7 +186,7 @@ std::string HelpExampleRpcNamed(const std::string& methodname, const RPCArgList&
186186
params.pushKV(param.first, param.second);
187187
}
188188

189-
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"1.0\", \"id\": \"curltest\", "
189+
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"2.0\", \"id\": \"curltest\", "
190190
"\"method\": \"" + methodname + "\", \"params\": " + params.write() + "}' -H 'content-type: text/plain;' http://127.0.0.1:8332/\n";
191191
}
192192

0 commit comments

Comments
 (0)