Skip to content

fix: StatelessHTTPServerTransport leaves the original POST hanging after notifications/cancelled (#255)#260

Open
ianegordon wants to merge 1 commit into
modelcontextprotocol:mainfrom
ianegordon:fix/255-cancellation-hang
Open

fix: StatelessHTTPServerTransport leaves the original POST hanging after notifications/cancelled (#255)#260
ianegordon wants to merge 1 commit into
modelcontextprotocol:mainfrom
ianegordon:fix/255-cancellation-hang

Conversation

@ianegordon

Copy link
Copy Markdown

Fixes #255.

Problem

A request cancelled via notifications/cancelled gets no JSON-RPC response (per the cancellation spec's "Not send a response for the cancelled request"), but nothing resumes the transport's HTTP waiter, so the original POST hangs until transport termination — violating the Streamable HTTP requirement that a request POST "MUST either return Content-Type: text/event-stream … or Content-Type: application/json, to return one JSON object."

Fix

The transport detects notifications/cancelled, still yields it to the Server (whose existing cancellation path cancels the handler task and stays wire-silent), then resumes the matching response waiter with a synthesized JSON-RPC error: code -32002, message "Request cancelled[: <reason>]". Deviating from the cancellation SHOULD is sanctioned by the spec itself: "The sender of the cancellation notification SHOULD ignore any response to the request that arrives afterward." Unknown, already-completed, and malformed cancellations are ignored ("Invalid cancellation notifications SHOULD be ignored"). Cancel-vs-response races are safe via actor isolation — exactly one path resumes the waiter.

-32002 sits in MCP's designated implementation-specific band [-32000, -32099], next after this SDK's -32000/-32001. Surveyed precedents: python-sdk always answers cancelled requests (code 0), go-sdk's jsonrpc2 always answers calls (generic -32001), kotlin-sdk completes the POST as 202-no-body (which reads as violating the MUST above for request inputs), typescript-sdk currently has this same hang. No SDK uses LSP's -32800.

Tests

Five deterministic tests (no fixed sleeps — handler-entry gating via AsyncStream): completion with -32002 + handler-observed cancellation, reason propagation, integer-id round-trip, unknown/completed-id no-op, malformed no-op. Full suite: 556 tests / 41 suites green, no warnings.

Non-goals

🤖 Generated with Claude Code

…essHTTPServerTransport

A request cancelled via notifications/cancelled correctly receives no
JSON-RPC response from Server, but nothing resumed the transport's HTTP
response waiter, so the original POST hung until transport termination.

The transport now detects notifications/cancelled, forwards it to the
Server unchanged (its existing cancellation path still cancels the
handler task), and resumes the matching waiter with a synthesized
JSON-RPC error response (code -32002, "Request cancelled"), satisfying
the Streamable HTTP requirement that a request POST receive one JSON
object. Unknown, already-completed, and requestId-less cancellations
are ignored per spec.

Fixes modelcontextprotocol#255

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sollahiro

Copy link
Copy Markdown

Verified locally: pinned swift-sdk to this PR's commit (b5da0ef) and ran our existing hang-reproduction test suite (StatelessTransportHangMitigationTests, built for #84/#254/#255) against it.

This is a local reproduction test, not a live production A/B — but the scenario it exercises (slow handler + notifications/cancelled) is exactly the mechanism we suspect caused the production symptom we reported (no server access-log entry, 8-79s client-side completion).

Results:

  • The raw transport now resumes the HTTP waiter (previously hung indefinitely; now completes in ~2.3s in our harness).
  • Our own mitigation (withOperationTimeout, 200s in prod) no longer needs to kick in — the request now completes with a -32002 JSON-RPC error in ~0.2s.
  • Full suite (719 tests) green aside from the two tests specifically asserting the old hang behavior (expected).

+1 from us based on this. We'll follow up if a production deploy surfaces anything further.

@ianegordon
ianegordon marked this pull request as ready for review July 20, 2026 22:31
@ianegordon

Copy link
Copy Markdown
Author

Thank you @sollahiro , marked as "Ready to Review"

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.

StatelessHTTPServerTransport: notifications/cancelled leaves the original POST hanging (HTTP exchange never completed)

2 participants