Skip to content

stdio transport: -32600 reply for an invalid JSON-RPC frame can be lost (AsyncRwTransport::receive is not cancel-safe) #1353

Open

Description

@qf0421

Summary

With the stdio transport (AsyncRwTransport), the -32600 Invalid Request reply that receive() sends for a well-formed JSON line that is not a valid JSON-RPC message is sometimes lost. The bad frame is consumed and never answered; the session otherwise continues normally.

Version: rmcp 3.5.1 (default-features = false, features = ["server", "transport-io"]), macOS arm64, Rust 1.95.0.

Cause

AsyncRwTransport::receive (src/transport/async_rw.rs) clears line_buf (line 160) and then, for a serde_json Data/Io error, awaits the writer lock (line 179) and sends the -32600 response from inside receive (lines 182-185).

The service loop polls transport.receive() inside tokio::select! together with response_send_tasks.join_next() (src/service.rs, lines 1412-1451). When the bad frame arrives right after a response, that response's send task can still hold the writer lock or be about to complete. If the join_next branch wins, the receive() future is dropped while it waits for the lock or while sending. The line has already been taken out of the buffer, so the error reply is never sent and nothing retries it.

receive is therefore not cancel-safe on this path. The existing comment covers cancellation of read_until only.

Reproduction

  1. Start a minimal stdio server (any ServerHandler) and complete initialize.
  2. Send {"jsonrpc":"2.0","id":1,"method":"ping"} and, without waiting, a line that parses as JSON but is not JSON-RPC, for example [1, 2] or {"id": 3, "method": "ping"} (no jsonrpc member).
  3. Wait for the -32600 reply.

Observed: the reply is missing in roughly 3-12% of attempts (4 of 40 and 2 of 60 in two runs). When the bad line is sent after a 150 ms pause, 0 of 100 replies were lost.

Expected

Every frame that receive decides to answer with -32600 gets that answer, regardless of what else the service loop is doing. Possible fixes: send the error reply outside the cancellable receive future (for example by returning it to the service loop, or via the regular response path), or make the error path cancel-safe by not consuming the line until the reply has been queued.

Related: #1347 (a known method whose params do not deserialize is answered with -32601).

Activity

  1. added
    bugSomething is not working
    P1High: significant functionality gap or spec violation
    T-transportTransport layer changes
    T-serviceService layer changes
    ready for workIssue is well-defined and ready to be picked up
    on Oct 14, 2026
  2. github-actions commented on Oct 14, 2026

    @github-actions
    Contributor

    @bolinfest is on triage rotation this week and is the reviewer for this one.

    Codex labeled this bug / P1 (T-transport, T-service). Please confirm or correct those labels.

    Automated triage. Labels are a first pass and the assigned maintainer has the final say.

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

Metadata

Metadata

Assignees

Labels

P1High: significant functionality gap or spec violationT-serviceService layer changesT-transportTransport layer changesbugSomething is not workingready for workIssue is well-defined and ready to be picked up

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions