Skip to content

fix(server): settle the POST for a cancelled request - #3107

Open
Sanath91009 wants to merge 2 commits into
modelcontextprotocol:mainfrom
Sanath91009:fix/cancelled-post-settles
Open

Sanath91009 wants to merge 2 commits into
modelcontextprotocol:mainfrom
Sanath91009:fix/cancelled-post-settles

Conversation

@Sanath91009

Copy link
Copy Markdown

Fixes #3053

After notifications/cancelled, WebStandardStreamableHTTPServerTransport never settles the original POST.

Reproduction on main

McpServer with one tool that waits for its request to be aborted, enableJsonResponse: true, POST tools/call, then cancel 50ms later:

cancel POST status: 202
original POST: still pending
streamMapping: 1 requestToStreamMapping: 1

Cause

Protocol._oncancel aborts the handler's controller. _onrequest then drops both the result and the error for that id when the signal is aborted, so send() never runs for it. The transport only clears _requestToStreamMapping, resolves resolveJson and closes the per-request SSE stream from send(). A cancelled request therefore keeps its stream mapping forever, and anything that counts in-flight work, expires idle sessions or caps concurrent sessions sees the session as permanently busy. Long agent runs cancel constantly, so the leak accumulates.

Fix

Transport local, no change to the Transport interface. Cancellation reaches the transport through onmessage, so the transport can settle the request itself when it sees notifications/cancelled.

  • A private _deliverMessage wrapper calls onmessage, then settles the cancelled request.
  • All three onmessage dispatch sites in handlePostRequest route through it, so a cancel sent in the same batch as its request is covered.
  • _settleCancelledRequest retires the id from both mappings. If an unresponded sibling still shares the stream, it leaves the stream open. Otherwise it resolves a JSON-mode POST with an empty 202, or cleans up the per-request SSE stream.

After the fix, the same reproduction gives:

original POST: settled 202 body=""
streamMapping: 0 requestToStreamMapping: 0

and the SSE variant ends the stream with read done=true bytes=0.

Tests

Three cases in packages/server/test/server/streamableHttp.test.ts:

  1. a cancelled JSON-mode POST settles with an empty 202 and removes its mappings
  2. a cancelled per-request SSE stream closes and removes its mappings
  3. a batch where one id is cancelled still delivers the completed sibling

All three fail on main (each times out waiting for a POST that never settles) and pass with the change. The whole packages/server suite is green: 55 files, 677 tests. Typecheck, ESLint and Prettier are clean.

No new dependencies.

A cancelled request kept its stream mapping forever because Protocol drops
the result and the error for an aborted request, so send() never runs and the
transport never resolves the POST or closes the per-request SSE stream.

Settle the request in the transport when notifications/cancelled arrives:
retire both mappings and either resolve the JSON response with the existing
empty 202 or clean up the SSE stream. An unresponded sibling that shares the
stream keeps it open.
@Sanath91009
Sanath91009 requested a review from a team as a code owner October 26, 2026 14:40
@changeset-bot

changeset-bot Bot commented Oct 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9d5d82

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/server Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/core-internal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@3107

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@3107

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@3107

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@3107

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@3107

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@3107

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@3107

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@3107

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@3107

commit: d9d5d82

@nosmile99

Copy link
Copy Markdown

Verified on this branch at d9d5d821 (checked out Sanath91009:fix/cancelled-post-settles), running the issue #3053 repro script against the workspace source:

  • JSON mode: cancelled POST settles with an empty 202; _streamMapping and _requestToStreamMapping both drop to 0 (pre-fix this was "still pending" / 1 / 1, as I verified on main @ 9bc40e9 on Oct 20).
  • SSE mode: the per-request stream ends (done=true), mappings 0 / 0.
  • Batched sibling: cancelling id 10 in a batch leaves id 11 to deliver its result normally; mappings 0 / 0.

Also ran packages/server/test/server/streamableHttp.test.ts: 60/60 pass, including the 3 new Cancellation tests. The diff matches the transport-local shape discussed on #3053 on Oct 23 — _deliverMessage at all three onmessage dispatch sites, no Transport interface change. No issues found from my side.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Oct 27, 2026

This branch has not been deployed

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

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP: the POST for a cancelled request never settles

3 participants