Skip to content

Add JSON-RPC batch support to channel_rpc and body reader#741

Closed
eynhaender wants to merge 1 commit intolibbitcoin:masterfrom
eynhaender:master
Closed

Add JSON-RPC batch support to channel_rpc and body reader#741
eynhaender wants to merge 1 commit intolibbitcoin:masterfrom
eynhaender:master

Conversation

@eynhaender
Copy link

Add JSON-RPC batch request support (Phase 1 — network layer)

Background

JSON-RPC 2.0 §6 allows a client to send an array of request objects in a single
message. Until now, any such message caused the channel to drop: the body reader
called boost::json::value_to<request_t>() on an array root, which threw, and
the resulting boost_code propagated as a read error.

This PR adds full batch support at the network layer. No changes to
libbitcoin-server are required for the Electrum TCP path; HTTP batch dispatch
is a separate follow-up (Phase 3).

Each response is sent as an independent socket write — one at a time, without
buffering all N responses server-side. This bounds memory use regardless of
batch size and eliminates a DoS vector. The client correlates responses by id
as they arrive.

What changed

Parsing (rpc/model.hpp, rpc/body.hpp, rpc/body.cpp)

  • model.hpp — adds using batch_t = std::vector<request_t>. No
    serialization tag_invoke is needed; the reader deserializes each element
    individually using the existing value_to<request_t>.

  • body.hpp — adds a full template specialization
    message_type<request_t> that carries both request_t message (single path)
    and batch_t batch (batch path), plus an is_batch() predicate. The generic
    message_type<T> template is untouched; message_type<response_t> is
    unaffected. Typedefs rpc::request, rpc::request_cptr, rpc::reader, etc.
    are unchanged.

  • body.cppbody<request_t>::reader::finish() gains a three-branch
    dispatch on the JSON root type:

    • object → existing single-request path (unchanged, including v1 semantic
      validation)
    • array → new batch path: rejects empty arrays (jsonrpc_batch_empty),
      rejects non-object elements (jsonrpc_batch_item_invalid), deserializes
      each element into value_.batch via the existing value_to<request_t>
    • anything elsejsonrpc_requires_method (unchanged from prior
      fallthrough)

    Error policy: fail-fast. A malformed batch is a protocol violation; the
    channel stops. No partial-batch recovery.

Dispatch (channel_rpc.hpp, channel_rpc.ipp)

  • channel_rpc.hpp — two new private members (batch_source_,
    batch_cursor_), a new virtual dispatch_batch(), and a non-virtual
    dispatch_next(). dispatch_batch() is virtual so derived channels can
    reject batch (e.g. stop(error::not_implemented)). dispatch_next() is
    non-virtual because the one-at-a-time sequencing is a correctness invariant,
    not a policy.

  • channel_rpc.ipp:

    • handle_receive() — routes to dispatch_batch() or the existing single
      path based on is_batch(). The prior TODO block is retained as RESOLVED:
      comments with a reference to the implementation.
    • dispatch_batch() — stores batch_source_ (shared_ptr, no copy) and calls
      dispatch_next().
    • dispatch_next() — while-loops past notification items (no id), then
      dispatches the next request item. On unknown method it calls send_error()
      and returns; handle_send() will advance the cursor. On batch exhaustion it
      resets state and calls receive().
    • handle_send() — the final receive() is replaced by a conditional:
      dispatch_next() if a batch is in flight, otherwise receive().
    • stopping() — resets batch_source_ and batch_cursor_ on stop.

Error codes (error.hpp, error.cpp)

Two new enumerators appended after jsonrpc_writer_exception:

Code Message When
jsonrpc_batch_empty json-rpc batch array must not be empty [] received
jsonrpc_batch_item_invalid json-rpc batch element is not a JSON object element is not a JSON object

Tests

test/messages/rpc/body_reader.cpp — 13 new cases inside HAVE_SLOW_TESTS:

  • message_type<request_t> specialization default state and is_batch() predicate
  • Single-request regression (object root path unchanged)
  • Batch happy paths: one element, two elements, notification (no id), HTTP
    path (no terminator)
  • Batch error paths: empty array, number element, array element, mixed
    valid/invalid, scalar root, string root

test/error.cpp — 2 new cases for jsonrpc_batch_empty and
jsonrpc_batch_item_invalid (value, truthiness, message string).

Note: the pre-existing test cases in body_reader.cpp use stale API names
(rpc::body::value_type, body.request, reader.is_done()) that do not match
the current code and will not compile. These are tracked separately and will be
fixed before HAVE_SLOW_TESTS is enabled in CI.

Channel-level dispatch tests (dispatch_batch, dispatch_next, handle_send
routing, stopping cleanup) require a strand-aware mock that does not yet
exist. Scenarios are documented in doc/json-rpc-batch-network.md.

What is NOT changed

  • rpc::request / rpc::request_cptr / rpc::reader typedefs — the
    specialization is transparent
  • rpc::response / message_type<response_t> — separate generic instantiation
  • http_body.hpp / http::body::reader::assign_reader()rpc::request now
    handles both cases internally; the HTTP body variant is untouched
  • dispatcher<I>::notify() — called per-item inside dispatch_next()
  • channel_rpc::dispatch() / receive() / send() / send_code() /
    send_result() / send_error() — all unchanged
  • protocol_rpc<Channel> and all derived protocol classes — no changes

@eynhaender eynhaender requested a review from evoskuil March 8, 2026 03:29
@eynhaender eynhaender self-assigned this Mar 8, 2026
@evoskuil
Copy link
Member

This fails to send the response an an array, so is only half complete.

@evoskuil evoskuil closed this Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants