Skip to content

Electrum RPC: return JSON-RPC 2.0 error objects instead of bare strings (port of mempool/electrs#103) #221

Description

@EddieHouston

Summary

Electrum RPC error replies currently put a bare string in the error member:

{"jsonrpc": "2.0", "id": 1, "error": "missing txid"}

JSON-RPC 2.0 requires error to be an object with integer code and string message
(plus optional data):

{"jsonrpc": "2.0", "id": 1, "error": {"code": -32602, "message": "missing txid"}}

Both sibling implementations have already moved to error objects, leaving this codebase
as the outlier:

  • mempool/electrs (fork of this codebase) did it in Fix JSON-RPC v2 errors mempool/electrs#103 (merged
    2024-11-14): -32601 for unknown methods, -32603 for handler errors, -32700 for
    unparseable JSON, -32600 for invalid request shapes — the latter two as replies
    (with "id" or null) instead of today's behavior of dropping the connection.
  • romanz/electrs (v0.9 rewrite) goes further with a typed RpcError enum that also
    distinguishes -32602 invalid params and ElectrumX-style application codes (1 bad
    request, 2 daemon error).

Without codes, clients cannot programmatically distinguish error categories (bad params
vs. unknown method vs. server-side failure) short of parsing message strings, and
malformed envelopes get a connection drop instead of any reply. There is a
long-standing TODO at the conversion site in handle_command
(src/electrum/server.rs) pointing at this.

Related: #220 fixed unknown methods dropping the connection; this issue covers the
shape of error replies in general.

Proposal

1. Port mempool/electrs#103. A trial cherry-pick of
mempool/electrs@277b05b onto current new-index shows
the change is mostly directly applicable:

  • Auto-merges cleanly: the JsonRpcV2Error enum codes, the json_rpc_error()
    helper, the -32601 catch-all for unknown methods, and the handler-error conversion
    (json_rpc_error(e, Some(id), InternalError) replacing the bare-string format!).
  • Trivial conflicts (both sides added code at the same spots): their enum/helper vs.
    this repo's RPC-event-logging macro and a #[trace] attribute. Resolution is keep
    both.
  • Needs a manual port: the envelope-error handling (-32700/-32600 replies).
    Their hunk is written against mempool's refactored connection layer
    (ConnectionStream, crossbeam channels, a handle_value returning Value), which
    this codebase doesn't have, and it must preserve this repo's RPC event logging inside
    handle_value. Roughly 30–50 lines re-implemented in the existing
    handle_value/handle_replies structure.

Porting the commit (rather than a fresh design) keeps the two esplora-lineage forks
behaviorally aligned and preserves the original authorship.

2. Distinguish error categories beyond the port. mempool/electrs#103 lumps all
handler errors under -32603 internal error. As part of the same change, distinguish:

  • -32602 invalid params, raised by the param helpers (usize_from_value,
    hash_from_value, ...) instead of generic message strings;
  • application-specific codes for cases like history-too-large or daemon failures,
    matching romanz/ElectrumX conventions (1 bad request, 2 daemon error).

This requires categorizing errors that currently all arrive as ErrorKind::Msg(String)
— new ErrorKind variants in src/errors.rs and a sweep over the handlers/param
helpers to use them.

Compatibility

This changes the wire contract for errors. Anything matching on error being a string
breaks — bespoke clients, test suites, monitoring keyed on error strings. Mitigations:

  • Most Electrum wallet clients already handle error objects, since ElectrumX, Fulcrum,
    romanz/electrs, and mempool/electrs all return them; the bare-string form here is the
    nonstandard one.
  • mempool/electrs has shipped this contract since late 2024 to a large client
    population without apparent fallout, which is decent field evidence.
  • Could be gated behind a config flag or a deprecation cycle if maintainers prefer a
    transition period.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions