fix: StatelessHTTPServerTransport leaves the original POST hanging after notifications/cancelled (#255)#260
Open
ianegordon wants to merge 1 commit into
Conversation
…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>
|
Verified locally: pinned swift-sdk to this PR's commit (b5da0ef) and ran our existing hang-reproduction test suite ( This is a local reproduction test, not a live production A/B — but the scenario it exercises (slow handler + Results:
+1 from us based on this. We'll follow up if a production deploy surfaces anything further. |
ianegordon
marked this pull request as ready for review
July 20, 2026 22:31
Author
|
Thank you @sollahiro , marked as "Ready to Review" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #255.
Problem
A request cancelled via
notifications/cancelledgets 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 returnContent-Type: text/event-stream… orContent-Type: application/json, to return one JSON object."Fix
The transport detects
notifications/cancelled, still yields it to theServer(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.-32002sits in MCP's designated implementation-specific band[-32000, -32099], next after this SDK's-32000/-32001. Surveyed precedents: python-sdk always answers cancelled requests (code0), 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