Skip to content

Feature: JSON-RPC 2.0 support #18

Description

@goduni

JSON-RPC 2.0 support

Motivation

Many APIs speak JSON-RPC 2.0 over HTTP. You can call them with unihttp today, but every project ends up re-writing the same envelope / id / error handling by hand.

It would be nice to have this out of the box — including batch requests, which are the hard part to do yourself.

What we want

Full JSON-RPC 2.0 over HTTP, in one PR:

  • Declare a JSON-RPC call in the same declarative style as a regular unihttp method
  • Get a typed result back, or a proper exception carrying code / message / data when the server returns error
  • Named and positional params — both "params": {"a": 1, "b": 2} and "params": [1, 2] (Ethereum and Bitcoin Core use the positional form almost exclusively)
  • Notifications — requests without id, for which the server sends no response
  • Batch requests — several calls in one HTTP request, with a typed result (or an error) for each of them; notifications may be part of a batch

Constraints

  • No changes to the core if possible. The existing BaseMethod hooks (build_http_request, validate_response, make_response) look sufficient — a new unihttp/jsonrpc/ package is the expected shape. If you think a core change is really needed, let's discuss it first.
  • Works with both sync and async clients, with bind_method and call_method
  • Middleware (retries, auth, logging) keeps working, and sees a batch as a single HTTP request
  • Follows the JSON-RPC 2.0 spec. In particular:
    • batch responses may come back in any order and must be matched by id
    • notifications get no entry in a batch response; a batch of only notifications gets no response body at all
    • a server may reject a whole batch with a single error object instead of an array
  • Works with every serialization backend (adaptix, pydantic, msgspec)
  • Tests, a docs page and an example in examples/

Open questions

The API is intentionally not fixed — this is the part we'd like your ideas on:

  1. Where does the RPC method name live? A class attribute, the __url__, something else?
  2. How are positional params and notifications declared? Class-level flags, separate base classes, markers?
  3. Where does id come from? Per method, per client, a counter, a UUID? Should it be configurable?
  4. How do batch results reach the caller with types preserved? Future-like handles, a gather-style tuple, something else?
  5. Partial failures in a batch: one call failed, the others succeeded — what does the caller see?
  6. Headers / query params in a batch: child calls may declare different ones, but there is only one HTTP request.
  7. Non-JSON error responses (e.g. an HTML 502 page): how should this interact with the existing on_error / handle_error flow?

How to contribute

Please post a short API sketch in a comment before writing code — a few lines showing how a method is declared, how it's called, and how a batch looks. We'll agree on the direction here, so nobody spends a weekend on a PR that needs to be redone.

Partial ideas are welcome too — you don't need to answer every question above.

Out of scope

  • Non-HTTP transports (WebSocket, stdio)

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions