rpc: JSON-RPC notifications, ids, statuses and the server timeout (#664) - #717
rpc: JSON-RPC notifications, ids, statuses and the server timeout (#664)#717bkeroack wants to merge 4 commits into
Conversation
|
Pushed one more commit ( The Core-functional run set caught it — Applying it to 2.0 as well is not cosmetic: Core's own
🤖 Generated with Claude Code |
|
One more commit (
🤖 Generated with Claude Code |
7aa92b3 to
6bd870a
Compare
b4493af to
97b9fff
Compare
6bd870a to
eeb1f01
Compare
97b9fff to
40f5653
Compare
A JSON-RPC 2.0 request with no `id` is a notification: the method runs and nothing comes back — 204 for a single one, no entry in a batch. satd's compatibility layer injected `"id": null` into every request that lacked one, because jsonrpsee will not run a request without an id, so it never saw a notification and the node always answered. Each notification now gets a distinguishable synthetic id whose reply is recognised and dropped. The `id` is echoed as Core echoes it. Core parses it as an optional and pushes it into the reply only when the request carried one, so `"id": null` and no id are different requests with different replies. satd omitted every null id, collapsing the two — and got the no-id case wrong for 1.0 clients, which Core answers with no `id` member at all rather than a null one. A second sentinel carries that distinction from the request, where it is knowable, to the response, where it is not. The HTTP status carries the error class. Core's `httprpc.cpp` maps a parse error to 500, an invalid request to 400 and an unknown method to 404; jsonrpsee answers 200 to all three, so a client that switches on the status could not tell them apart. `-rpcservertimeout` bounded one phase of one listener: hyper's header-read timeout on the plain HTTP/1.1 path and nothing else. A client that sent a complete head and then no body, or held an idle keep-alive connection, or spoke HTTP/2, was never disconnected — and the TLS listener used jsonrpsee's helper, which takes no timeout, so a TLS connection had none at all. The connection server is generic over the transport now and both surfaces use it. Finally, a response too large to normalise is forwarded in jsonrpsee's shape rather than DOM-parsed to touch three top-level keys, which costs several times its size again. A verbosity-2 `getblock` over a full block is the realistic case; truncating or refusing it would be worse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL
Caught by the Core-functional run set, not by this branch's own tests:
`rpc_generate.py` and `wallet_disable.py` both regressed to failing.
Core maps a JSON-RPC error code onto an HTTP status only for a legacy
(1.0 / 1.1 / absent-`jsonrpc`) request. `HTTPReq_JSONRPC` runs a 2.0
request with `catch_errors{jreq.m_json_version == JSONRPCVersion::V2}`
and replies HTTP 200 with the error in the body; `JSONErrorReply`, the
only place the mapping lives, opens with
`Assume(jreq.m_json_version != JSONRPCVersion::V2)`.
Applying it to 2.0 as well is not cosmetic. Core's own `authproxy`
raises `-342 non-200 HTTP status code` for a 2.0 reply whose status is
not 200 *before* it looks at the error object, so the real code never
reaches the caller — which is exactly how those two tests broke, both
of them asserting on `-32601`.
`core_http_status` now takes the request's version so the rule is
testable, and the existing test asserts both halves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL
) The regtest test written alongside the HTTP status mapping sent a JSON-RPC **2.0** request and asserted 404. It was passing because the mapping applied to 2.0 too, which is the defect `5bc1fdba` fixed — and I did not re-run the suite after that fix, so the stale assertion survived until the next branch's gates caught it. It now pins both halves: a legacy request carries the class in the status, a 2.0 request keeps 200 and carries the code in the body. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL
…s it `JSONErrorReply` (`httprpc.cpp`) starts from `HTTP_INTERNAL_SERVER_ERROR` and overrides exactly two codes: -32600 → 400 and -32601 → 404. satd mapped those plus the parse error and left every application error — -8, -5, -32602 — at 200, so `getblockhash [99999999]` on a 1.0 request answered 200 where Core answers 500; `interface_rpc.py` asserts that call. The regtest test pinned the wrong behaviour and is corrected. The mapping parses the reply, so it now stops at the same cap the 1.0 normalisation does: a reply past the cap is a result, not an error object, and parsing it for a status code was the DOM parse the cap exists to avoid. Release-note wording on `-rpcservertimeout` corrected: it bounds idle keep-alive and h2 as well as the header read, but not a request whose head arrived and whose body never completes, nor a long-running handler. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL
eeb1f01 to
85be3a0
Compare
40f5653 to
6faab7c
Compare
|
Independent review pass — one fix landed on this branch.
Verified against Core v31.1 🤖 Generated with Claude Code |
Closes #664.
Seventh in the stack, based on #716. Merge order: #709 → #710 → #711 → #712 → #713 → #716 → this. (#715 is independent and unblocks CI.)
A 2.0 request with no
idis a notificationThe method runs and nothing comes back: HTTP 204 for a single one, and no entry in a batch. satd's compatibility layer injected
"id": nullinto every request that lacked one — jsonrpsee will not run a request without an id — so it never saw a notification and the node always answered.Each notification now gets a distinguishable synthetic id, and its reply is recognised and dropped. A shared
nullwould not do: two notifications in one batch would come back indistinguishable.idis echoed as Core echoes itCore parses
idas anoptionaland pushes it into the reply only when the request carried one (rpc/request.cpp,JSONRPCReplyObj). So"id": nulland no id are different requests with different replies — the first is echoednull, the second gets noidmember at all.satd omitted every null id, which collapsed the two, and got the no-id case wrong for 1.0 clients. A second sentinel carries that distinction from the request, where it is knowable, to the response, where it is not.
The HTTP status carries the error class
Core's
httprpc.cppmaps a parse error to 500, an invalid request to 400 and an unknown method to 404. jsonrpsee answers 200 to all three, so a client that switches on the status — Core's owninterface_rpc.pydoes — could not tell them apart. An application error keeps 200, as Core does, and a batch is unaffected.-rpcservertimeoutbounded one phase of one listenerIt set hyper's header-read timeout on the plain HTTP/1.1 path and nothing else. A client that sent a complete head and then no body, or held an idle keep-alive connection open, or spoke HTTP/2, was never disconnected — and the TLS listener used jsonrpsee's
serve_with_graceful_shutdown, which takes no timeout at all, so a TLS connection had none.serve_http_connectionis generic over the transport now, so both surfaces use it, and the budget applies to keep-alive and to h2 as well.A very large response is no longer DOM-parsed
Normalising a reply to JSON-RPC 1.0 means parsing it to touch three top-level keys, which costs several times its size again; a
getblockat verbosity 2 over a full block is the realistic case. Past the body cap the reply is forwarded in jsonrpsee's shape instead. Truncating or refusing it would be worse: the answer is correct JSON, only its envelope is 2.0.Verification
Three cargo gates green. Every guard perturbation-proved by a named failing test: notification detection, the response-id strip, the old omit-every-null rule, keeping notification replies in a batch, and the status mapping.
Four unit tests and three regtest tests over the wire — a lone notification is 204 with an empty body while the same request with an id answers normally; a 1.0 request without an id is answered with no
idmember and an explicit null is echoed; a batch drops only the notification; and the three status codes.interface_rpc.pywas re-measured: notifications, batch entries, id echoing and the status mapping all pass now, and the remaining failure istest_getrpcinfoasserting one entry inactive_commands, which satd still reports as[]. That is #702's, and the row's note names it.🤖 Generated with Claude Code
https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL