Repository navigation
fix: StatelessHTTPServerTransport leaves the original POST hanging after notifications/cancelled (#255) - #260
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. |
|
Thank you @sollahiro , marked as "Ready to Review" |
…tream modelcontextprotocol#260, rebased onto modelcontextprotocol#264) Manifest entry 2. Upstream refs/pull/260/head b5da0ef (branch pr/260), rebased onto modelcontextprotocol#264 as 12b8c92 with the cancellation routed through the exchange table. Fixes modelcontextprotocol#255.
…otocol#264) as manifest entry 2
…ests without a reply (upstream modelcontextprotocol#298, EhsanAzish80) Supersedes the contributions of modelcontextprotocol#264, modelcontextprotocol#260 and modelcontextprotocol#270 (manifest entries 1-3), which modelcontextprotocol#298 combines. Conflicts resolved in favour of modelcontextprotocol#298 in HTTPServerTypes.swift, StatelessHTTPServerTransport.swift, Server.swift, CancellationTests.swift and HTTPServerTransportTests.swift. Fork changes re-applied on top: - modelcontextprotocol#258's Server.init description/websiteUrl/icons parameters - modelcontextprotocol#221's MockTransport receiveCallCount / finishStream - entry 1a's collision regression tests and their makeStatelessPOSTRequest authorization overload Behavior: a cancelled original POST now ends as HTTP 200 with an empty text/event-stream and no JSON-RPC body (previously -32002 from modelcontextprotocol#260). Follow-up on when to use -32800: fork issue #41. Full suite: 589 tests in 43 suites passed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Hi @ianegordon, thank you for this contribution. We're consolidating overlapping PRs. #298 covers the same issue (#255), completing the original POST after We plan to close this PR once #298 is merged, so the fix isn't lost in between. If you think something here isn't covered by #298, please comment there or here and we'll make sure it's carried over. |
Motivation and Context
Fix #255 - StatelessHTTPServerTransport: notifications/cancelled leaves the original POST hanging (HTTP exchange never completed)
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
How Has This Been Tested?
Tested locally AND @sollahiro verified fix addresses a hang in their test suite.
Breaking Changes
None
Types of changes
Checklist
🤖 Generated with Claude Code and Codex