rpc: handle malformed jsonrpc 2.0 requests correctly #36758

pull pinheadmz wants to merge 2 commits into bitcoin:master from pinheadmz:jsonrpc-assume-2.0 changing 4 files +45 −7
  1. pinheadmz commented at 3:20 PM on November 4, 2026: member

    While reviewing #36642 I found something else we missed in JSON-RPC 2.0:

    If we receive a malformed request we would assume that we couldn't parse it's jsonrpc version and send an HTTP error. This is how the legacy jsonrpc server worked (despite that behavior not ever being part of any ratified jsonrpc spec) and we maintain it as Bitcoin "legacy" jsonrpc protocol. However it's possible to form a request that does properly indicate jsonrpc 2.0 but is still malformed. In that case, we should not send an HTTP error: the correct protocol is to respond with HTTP 200 OK and include the jsonrpc error message in the response.

    The solution is to check if we know the request's jsonrpc version before responding with an error.

    We already correctly handle such malformed requests in batches and this is already tested in test_batch_requests() in interface_rpc.py

    On master, the functional test in this PR actually crashes the node because the assumption is codified. Note that this crash requires:

    1. RPC Authentication
    2. A debug build

    So this is not an urgent security fix, but a bug that might disrupt testing.

    node0 stderr httprpc.cpp:46
        UniValue JSONErrorReply(UniValue, const JSONRPCRequest &, HTTPStatusCode &):
            'jreq.m_json_version != JSONRPCVersion::V2' check failed.
    

    In release builds the node doesn't crash, but it returns HTTP 400 with a JSON-RPC 2.0 error body instead of HTTP 200.

  2. in src/httprpc.cpp:127 in 02380237dd
     123 | +                if (jreq.IsNotification()) {
     124 | +                    status = HTTP_NO_CONTENT;
     125 | +                    return {};
     126 | +                }
     127 | +                return JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
     128 | +            }
    


    ryanofsky commented at 8:02 PM on November 4, 2026:

    In commit "rpc: handle malformed jsonrpc 2.0 requests correctly" (02380237ddd2ed6878d7a3b0ae2807cfcf2cd9ff)

    Instead of adding a third place that converts a caught error into a reply (with the batch loop and the outer catch), it could be simpler to make JSONErrorReply() do the right thing for 2.0 requests, replacing the Assume with the behavior it asserts. Then this parse() call can stay as it was, and any error thrown after the version is known gets the 2.0 treatment, not just parse errors. Suggested diff from master:

    --- a/src/httprpc.cpp
    +++ b/src/httprpc.cpp
    @@ -40,10 +40,17 @@ static bool g_rpc_whitelist_default = false;
    
     static UniValue JSONErrorReply(UniValue objError, const JSONRPCRequest& jreq, HTTPStatusCode& nStatus)
     {
    -    // HTTP errors should never be returned if JSON-RPC v2 was requested. This
    -    // function should only be called when a v1 request fails or when a request
    -    // cannot be parsed, so the version is unknown.
    -    Assume(jreq.m_json_version != JSONRPCVersion::V2);
    +    // A request known to be JSON-RPC 2.0 gets the error in the reply with HTTP
    +    // success, or no reply if it is a notification. HTTP error statuses are
    +    // only returned for 1.x requests and requests whose version is unknown.
    +    if (jreq.m_json_version == JSONRPCVersion::V2) {
    +        if (jreq.IsNotification()) {
    +            nStatus = HTTP_NO_CONTENT;
    +            return {};
    +        }
    +        nStatus = HTTP_OK;
    +        return JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version);
    +    }
    
         // Send error reply from json-rpc error object
         nStatus = HTTP_INTERNAL_SERVER_ERROR;
    

    This would also help #36753, which currently builds the 2.0 reply for a whitelist denial itself and could just throw the error for both versions.

    I tested this diff together with the PR's test changes: interface_rpc.py, including the new tests, and rpc_whitelist.py pass in a debug build.


    pinheadmz commented at 3:31 PM on November 5, 2026:

    Much better approach thanks, taking.

  3. in test/functional/interface_rpc.py:239 in 02380237dd
     232 | @@ -224,6 +233,10 @@ def test_http_status_codes(self):
     233 |          expect_http_rpc_status(204, None, self.nodes[0], "generatetoaddress", [1, "invalid_address"], 2, True)
     234 |          # Sanity check: command was not executed
     235 |          assert_equal(block_count + 1, self.nodes[0].getblockcount())
     236 | +        # No error response for notifications that fail to parse either
     237 | +        response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "2.0"})
     238 | +        assert_equal(response, None)
     239 | +        assert_equal(status, 204)
    


    ryanofsky commented at 8:02 PM on November 4, 2026:

    In commit "rpc: handle malformed jsonrpc 2.0 requests correctly" (02380237ddd2ed6878d7a3b0ae2807cfcf2cd9ff)

    Not a blocker, but if we want to comply with the spec it seems like this case should get an error reply instead of 204. The JSON-RPC 2.0 spec's examples answer an invalid Request object with no id ({"jsonrpc": "2.0", "method": 1, "params": "bar"}) with an error and "id": null, treating it as an invalid request rather than a notification. If we don't want to comply with the spec, the test comment should be updated to say this is intentional. The test comment suggestion includes that.

    Note that compatibility with previous bitcoin core releases shouldn't be a concern in this case since before this PR this request made debug builds abort on the Assume in JSONErrorReply(), and release builds returned HTTP 400 with a "Missing method" error and no id.


    pinheadmz commented at 6:33 PM on November 5, 2026:

    Great catch and essentially another jsonrpc 2.0 bug from the spec. the second commit now ensures there is always an id in 2.0 responses. This makes the test simpler too since all the malformed body cases have the same outcome.


    ryanofsky commented at 9:03 PM on November 6, 2026:

    re: #36758 (review)

    the second commit now ensures there is always an id in 2.0 responses

    Thanks, this looks right for single requests. Batches still drop these requests, though, see the comment about batches.

  4. in test/functional/interface_rpc.py:216 in 02380237dd
     211 | +            ({"method": 1}, "Method must be a string"),
     212 | +            ({"method": "getblockcount", "params": 1}, "Params must be an array or object"),
     213 | +        ]:
     214 | +            response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "2.0", "id": 1, **request})
     215 | +            assert_equal(response, {"jsonrpc": "2.0", "error": {"code": RPC_INVALID_REQUEST, "message": message}, "id": 1})
     216 | +            assert_equal(status, 200)
    


    ryanofsky commented at 8:02 PM on November 4, 2026:

    In commit "rpc: handle malformed jsonrpc 2.0 requests correctly" (02380237ddd2ed6878d7a3b0ae2807cfcf2cd9ff)

    nit: The JSON-RPC 1.1 vs 2.0 table says 2.0 requests get HTTP 200 "unless there is an actual HTTP server error (request parsing error, endpoint not found, etc)", which could be read as covering the malformed requests tested here. Probably makes sense to update that documentation to say "request body is not valid JSON" instead, to match the behavior after this PR.


    ryanofsky commented at 8:13 PM on November 4, 2026:

    In commit "rpc: handle malformed jsonrpc 2.0 requests correctly" (02380237ddd2ed6878d7a3b0ae2807cfcf2cd9ff)

    Since this commit only adds test cases, it's hard to tell from the diff what behavior it changes. Could the commit message say what these requests did before? The PR description explains it, but the commit only has a subject line. Something like:

    A JSON-RPC 2.0 request that fails to parse after its "jsonrpc": "2.0" field
    has been read, for example one with a missing or non-string method or
    invalid params, reached the Assume in JSONErrorReply(). Debug builds
    aborted, and release builds returned HTTP 400 with a 2.0 error body.
    
    Return these errors like any other 2.0 error instead: HTTP 200 with the
    error in the reply, or HTTP 204 and no reply for a request without an id.
    

    The test comments could explain more too. The notification case confused me most: it wasn't clear why {"jsonrpc": "2.0"} fails to parse (it has no method), or how this differs from the invalid-notification case just above it, which parses fine and fails when the method runs. Also, since this case doesn't follow the JSON-RPC 2.0 spec (see the separate comment about it), the test should say so, so the behavior isn't mistaken for what the spec requires. Maybe:

    --- a/test/functional/interface_rpc.py
    +++ b/test/functional/interface_rpc.py
    @@ -205,7 +205,8 @@ class RPCInterfaceTest(BitcoinTestFramework):
             response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "3.0", "method": "getblockcount"})
             assert_equal(response, {"result": None, "error": {"code": RPC_INVALID_REQUEST, "message": "JSON-RPC version not supported"}})
             assert_equal(status, 400)
    -        # invalid requests that are known to be 2.0 get RPC errors but not HTTP errors
    +        # Malformed requests that are known to be JSON-RPC 2.0, because they
    +        # have "jsonrpc": "2.0", get RPC errors with HTTP 200, not HTTP errors.
             for request, message in [
                 ({}, "Missing method"),
                 ({"method": 1}, "Method must be a string"),
    @@ -233,7 +234,11 @@ class RPCInterfaceTest(BitcoinTestFramework):
             expect_http_rpc_status(204, None, self.nodes[0], "generatetoaddress", [1, "invalid_address"], 2, True)
             # Sanity check: command was not executed
             assert_equal(block_count + 1, self.nodes[0].getblockcount())
    -        # No error response for notifications that fail to parse either
    +        # No response either for a request without an id that fails to parse,
    +        # here because it has no method, so the error happens before any method
    +        # runs. This does not follow the JSON-RPC 2.0 spec, which treats an
    +        # invalid request as not being a notification and replies with an error
    +        # and "id": null.
             response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "2.0"})
             assert_equal(response, None)
             assert_equal(status, 204)
    

    pinheadmz commented at 3:43 PM on November 5, 2026:

    good catch! adding that change to the table in the doc


    pinheadmz commented at 5:41 PM on November 5, 2026:

    I'll break out the tests into a test commit that asserts the current behavior, then update the test with the patch. Will clean up the comments too as you suggest.


    ryanofsky commented at 9:03 PM on November 6, 2026:

    re: #36758 (review)

    I'll break out the tests into a test commit that asserts the current behavior

    Thanks, this is better than what I suggested. I didn't think a test commit could show the old behavior since debug builds abort, but catching the disconnect and checking stderr for the failed Assume works well, and the test diff in the second commit now shows exactly what changed.

  5. ryanofsky approved
  6. ryanofsky commented at 8:36 PM on November 4, 2026: contributor

    Code review ACK 02380237ddd2ed6878d7a3b0ae2807cfcf2cd9ff. Thanks for finding and fixing this!

    It makes sense to merge this before #36753, which moves the -rpcwhitelistdefault check after parsing, so more users can reach this code. I'll rebase that PR on this one.

    The suggestions below are all optional. The main one is to fix this in JSONErrorReply() instead of adding another try/catch, so it covers every error thrown after the version is known, and #36753 could use it as well.

  7. pinheadmz force-pushed on Nov 5, 2026
  8. DrahtBot commented at 7:04 PM on November 5, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Stale ACK ryanofsky

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  9. pinheadmz commented at 7:05 PM on November 5, 2026: member

    push to 9b21a79587e130f0c51cc0ffc570c1d418258615

    Address feedback from @ryanofsky

    • move logic into JSONErrorReply
    • add tests first to assert current behavior
    • update docs
    • fix another jsonrpc2.0 bug (responding to malformed notifications with id)
  10. DrahtBot added the label RPC/REST/ZMQ on Nov 5, 2026
  11. pinheadmz commented at 3:17 PM on November 6, 2026: member

    Ralph review mentions -rpcwhitelist which is handled in #36753

  12. in doc/JSON-RPC-interface.md:93 in 9b21a79587
      88 | @@ -89,7 +89,7 @@ protocol in v27.0 and prior releases.
      89 |  | Request marker | `"version": "1.1"` (or none) | `"jsonrpc": "2.0"` |
      90 |  | Response marker | (none) | `"jsonrpc": "2.0"` |
      91 |  | `"error"` and `"result"` fields in response | both present | only one is present |
      92 | -| 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) |
      93 | +| 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 body is not valid JSON, endpoint not found, etc) |
      94 |  | Notifications: requests that get no reply | (not supported) | Supported for requests that exclude the "id" field. Returns HTTP status `204` "No Content" |
    


    ryanofsky commented at 9:03 PM on November 6, 2026:

    In commit "rpc: handle malformed jsonrpc 2.0 requests correctly" (9b21a79587e130f0c51cc0ffc570c1d418258615)

    nit: The Notifications row says any request without an "id" field is a notification, but after this PR an invalid one gets an error reply, because the spec only counts valid Request objects as notifications. Maybe:

    | Notifications: requests that get no reply | (not supported) | Supported for valid requests that exclude the "id" field. Returns HTTP status `204` "No Content". Invalid requests are not notifications, and get an error reply with `"id": null` even without an "id" field |
    

    pinheadmz commented at 7:38 PM on November 9, 2026:

    I'm just gonna throw in the qualifier "valid" (requests) here but if you wanna push for the whole sentence I'll add it.

  13. in test/functional/interface_rpc.py:241 in 9b21a79587 outdated
     236 | +            response, status = send_json_rpc(self.nodes[0], {**request})
     237 | +            assert_equal(
     238 | +                response,
     239 | +                {'result': None, 'error': {'code': -32600, 'message': message}}
     240 | +            )
     241 | +            assert_equal(status, 400)
    


    ryanofsky commented at 9:03 PM on November 6, 2026:

    In commit "test: assert current RPC behavior when handling malformed request body" (bb036a9150ad79d6c6c9cc1ee225842874b6af9f)

    nit: {**request} makes a copy that isn't needed, since the dict isn't modified, and -32600 could be RPC_INVALID_REQUEST like in the 2.0 loop below:

    for request, message in cases:
        response, status = send_json_rpc(self.nodes[0], request)
        assert_equal(response, {"result": None, "error": {"code": RPC_INVALID_REQUEST, "message": message}})
        assert_equal(status, 400)
    

    pinheadmz commented at 7:32 PM on November 9, 2026:

    Cleaning this up as suggested, thanks

  14. in src/httprpc.cpp:48 in 9b21a79587 outdated
      47 | +    // A request known to be JSON-RPC 2.0 gets the error in the reply with HTTP OK
      48 | +    // HTTP error statuses are only returned for 1.x requests and requests whose version is unknown.
      49 | +    if (jreq.m_json_version == JSONRPCVersion::V2) {
      50 | +        nStatus = HTTP_OK;
      51 | +        return JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version);
      52 | +    }
    


    ryanofsky commented at 10:20 PM on November 6, 2026:

    In commit "rpc: handle malformed jsonrpc 2.0 requests correctly" (9b21a79587e130f0c51cc0ffc570c1d418258615)

    After this push, single requests and batches use different definitions of a notification. {"jsonrpc": "2.0"} gets an error reply with "id": null, as the spec requires, but [{"jsonrpc": "2.0"}] still gets HTTP 204 and no reply, because the batch loop treats any 2.0 entry without an id as a notification. The spec's batch example answers invalid entries with an error too.

    I think it would be better not to leave it half-done: either fix batches too, or go back to the previous behavior for single requests (the previous push, or my earlier suggestion, which kept the 204) and change the notification rule in a separate PR. Fixing it everywhere seems simplest. If parse() decides whether a request is a notification, IsNotification() is only true for valid requests, the batch loop needs no changes, and JSONErrorReply() can return 204 for notifications, so #36753 can throw the same error for both versions. Diff from master:

    --- a/src/httprpc.cpp
    +++ b/src/httprpc.cpp
    @@ -40,10 +40,17 @@ static bool g_rpc_whitelist_default = false;
    
     static UniValue JSONErrorReply(UniValue objError, const JSONRPCRequest& jreq, HTTPStatusCode& nStatus)
     {
    -    // HTTP errors should never be returned if JSON-RPC v2 was requested. This
    -    // function should only be called when a v1 request fails or when a request
    -    // cannot be parsed, so the version is unknown.
    -    Assume(jreq.m_json_version != JSONRPCVersion::V2);
    +    // A request known to be JSON-RPC 2.0 gets the error in the reply with HTTP
    +    // OK, or no reply if it is a notification. HTTP error statuses are only
    +    // returned for 1.x requests and requests whose version is unknown.
    +    if (jreq.m_json_version == JSONRPCVersion::V2) {
    +        if (jreq.IsNotification()) {
    +            nStatus = HTTP_NO_CONTENT;
    +            return {};
    +        }
    +        nStatus = HTTP_OK;
    +        return JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version);
    +    }
    
         // Send error reply from json-rpc error object
         nStatus = HTTP_INTERNAL_SERVER_ERROR;
    --- a/src/rpc/request.cpp
    +++ b/src/rpc/request.cpp
    @@ -68,7 +68,17 @@ UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue
             if (jsonrpc_version == JSONRPCVersion::V1_LEGACY) reply.pushKV("result", NullUniValue);
             reply.pushKV("error", std::move(error));
         }
    -    if (id.has_value()) reply.pushKV("id", std::move(id.value()));
    +    if (id.has_value()) {
    +        reply.pushKV("id", std::move(id.value()));
    +    } else if (jsonrpc_version == JSONRPCVersion::V2) {
    +        // The request had no "id" member. This happens for notifications, and
    +        // for invalid requests that left out the id (for example with a missing
    +        // or non-string method name). The JSON-RPC 2.0 spec requires every
    +        // response to have an "id", set to null when the request's id could not
    +        // be determined, so add "id": null. For notifications this is harmless,
    +        // because their replies are never sent.
    +        reply.pushKV("id", NullUniValue);
    +    }
         return reply;
     }
    
    @@ -200,6 +210,13 @@ std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue& in)
    
     void JSONRPCRequest::parse(const UniValue& valRequest)
     {
    +    // Reset fields set by any previous request in the same batch, so if this
    +    // request is invalid, its error reply does not use the previous id or
    +    // version.
    +    id = UniValue::VNULL;
    +    m_json_version = JSONRPCVersion::V1_LEGACY;
    +    m_notification = false;
    +
         // Parse request
         if (!valRequest.isObject())
             throw JSONRPCError(RPC_INVALID_REQUEST, "Invalid Request object");
    @@ -213,7 +230,6 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
         }
    
         // Check for JSON-RPC 2.0 (default 1.1)
    -    m_json_version = JSONRPCVersion::V1_LEGACY;
         const UniValue& jsonrpc_version = request.find_value("jsonrpc");
         if (!jsonrpc_version.isNull()) {
             if (!jsonrpc_version.isStr()) {
    @@ -254,4 +270,9 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
             params = UniValue(UniValue::VARR);
         else
             throw JSONRPCError(RPC_INVALID_REQUEST, "Params must be an array or object");
    +
    +    // A notification is a valid JSON-RPC 2.0 request without an "id" member.
    +    // Invalid requests are not notifications, and get an error reply even
    +    // without an id.
    +    m_notification = m_json_version == JSONRPCVersion::V2 && !id.has_value();
     }
    --- a/src/rpc/request.h
    +++ b/src/rpc/request.h
    @@ -63,9 +63,11 @@ public:
         std::string peerAddr;
         std::any context;
         JSONRPCVersion m_json_version = JSONRPCVersion::V1_LEGACY;
    +    //! Set by parse() for a valid JSON-RPC 2.0 request without an "id" member.
    +    bool m_notification{false};
    
         void parse(const UniValue& valRequest);
    -    [[nodiscard]] bool IsNotification() const { return !id.has_value() && m_json_version == JSONRPCVersion::V2; };
    +    [[nodiscard]] bool IsNotification() const { return m_notification; };
     };
    
     #endif // BITCOIN_RPC_REQUEST_H
    

    The HTTP_NO_CONTENT branch in JSONErrorReply() isn't reachable yet, since valid 2.0 notifications never get there on this branch, but it makes the function correct for every 2.0 request. The JSONRPCReplyObj() comment is the one from the separate comment about it.

    Resetting the fields at the start of parse() also fixes a bug that is already on master: a batch entry that isn't an object gets an error reply with the id and version of the entry before it. [{"jsonrpc": "2.0", "method": "getblockcount", "id": 5}, 1] gets "id": 5 for the 1, and [{"jsonrpc": "2.0", "method": "getblockcount"}, 1] gets no reply at all.

    I tested this with the PR's tests plus the test changes below, which expect an error for the invalid notification in the batch tests and cover the reset. interface_rpc.py, rpc_whitelist.py, interface_http.py and rpc_users.py pass in a debug build.

    <details><summary>Test changes</summary>

    --- a/test/functional/interface_rpc.py
    +++ b/test/functional/interface_rpc.py
    @@ -43,12 +43,16 @@ def format_request(options, idx, fields):
         return request
    
    
    -def format_response(options, idx, fields):
    -    if options.version == 2 and options.notification:
    +def format_response(options, idx, fields, invalid=False):
    +    # Valid 2.0 notifications get no response. Invalid requests are not
    +    # notifications, and get an error response with "id": null.
    +    if options.version == 2 and options.notification and not invalid:
             return None
         response = {}
         if not options.notification:
             response.update(id=idx)
    +    elif options.version == 2:
    +        response.update(id=None)
         if options.version == 2:
             response.update(jsonrpc="2.0")
         else:
    @@ -130,7 +134,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
                 if options is None:
                     continue
                 request.append(format_request(options, idx, call))
    -            r = format_response(options, idx, result)
    +            r = format_response(options, idx, result, invalid="method" not in call)
                 if r is not None:
                     response.append(r)
    
    @@ -176,6 +180,17 @@ class RPCInterfaceTest(BitcoinTestFramework):
                 request_fields={"jsonrpc": "2.1"},
                 response_fields={"result": None, "error": {"code": RPC_INVALID_REQUEST, "message": "JSON-RPC version not supported"}}))
    
    +    def test_batch_invalid_entries(self):
    +        self.log.info("Testing that invalid batch entries do not reuse the previous entry's id...")
    +        valid = {"jsonrpc": "2.0", "method": "getblockcount"}
    +        error = {"result": None, "error": {"code": RPC_INVALID_REQUEST, "message": "Invalid Request object"}, "id": None}
    +        response, status = send_json_rpc(self.nodes[0], [{**valid, "id": 5}, 1])
    +        assert_equal(response, [{"jsonrpc": "2.0", "result": 0, "id": 5}, error])
    +        assert_equal(status, 200)
    +        response, status = send_json_rpc(self.nodes[0], [valid, 1])
    +        assert_equal(response, [error])
    +        assert_equal(status, 200)
    +
         def test_http_status_codes(self):
             self.log.info("Testing HTTP status codes for JSON-RPC 1.1 requests...")
             # OK
    @@ -269,6 +284,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
         def run_test(self):
             self.test_getrpcinfo()
             self.test_batch_requests()
    +        self.test_batch_invalid_entries()
             self.test_http_status_codes()
             self.test_work_queue_exceeded()
    
    

    </details>


    pinheadmz commented at 7:54 PM on November 9, 2026:

    Heh ok this side bug is addressed in #36642 so I will focus on reviewing that, then rebase this on top to address the rest of your comment here.

  15. in src/rpc/request.cpp:75 in 9b21a79587
      67 | @@ -68,7 +68,13 @@ UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue
      68 |          if (jsonrpc_version == JSONRPCVersion::V1_LEGACY) reply.pushKV("result", NullUniValue);
      69 |          reply.pushKV("error", std::move(error));
      70 |      }
      71 | -    if (id.has_value()) reply.pushKV("id", std::move(id.value()));
      72 | +    if (id.has_value()) {
      73 | +        reply.pushKV("id", std::move(id.value()));
      74 | +    } else if (jsonrpc_version == JSONRPCVersion::V2) {
      75 | +        // id is required in JSONRPC 2.0 responses, can be null if there
      76 | +        // was an error parsing the request.
    


    ryanofsky commented at 10:20 PM on November 6, 2026:

    In commit "rpc: handle malformed jsonrpc 2.0 requests correctly" (9b21a79587e130f0c51cc0ffc570c1d418258615)

    This comment was hard for me to follow. It doesn't say the branch is for requests that had no "id" member, and it doesn't mention notifications, which also get here. Maybe:

            // The request had no "id" member. This happens for notifications, and
            // for invalid requests that left out the id (for example with a missing
            // or non-string method name). The JSON-RPC 2.0 spec requires every
            // response to have an "id", set to null when the request's id could not
            // be determined, so add "id": null. For notifications this is harmless,
            // because their replies are never sent.
    

    pinheadmz commented at 7:42 PM on November 9, 2026:

    Very clear thanks

  16. ryanofsky commented at 10:30 PM on November 6, 2026: contributor

    Thanks for the update! Moving the fix into JSONErrorReply() and the new test commit both look good, and thanks for fixing the missing "id" too.

    I'm not re-ACKing yet because single requests and batches now disagree about what a notification is. I'd be happy with either of the options there: fixing batches too, or going back to the previous behavior for single requests and changing the notification rule separately. The other new comments are optional.

  17. test: assert current RPC behavior when handling malformed request body 36a0ac026e
  18. rpc: handle malformed jsonrpc 2.0 requests correctly f27a0bc523
  19. pinheadmz force-pushed on Nov 9, 2026
  20. pinheadmz commented at 7:55 PM on November 9, 2026: member

    push to f27a0bc5234d5117981d4723af1ec910efbe353c:

    Clean up a test and some comments to address review by @ryanofsky however the biggest suggestion I did not take yet because we should review #36642 as prerequisitie.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-11-10 20:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me