Repository navigation
fix(server): settle the POST for a cancelled request - #3107
Sanath91009 wants to merge 4 commits into
Conversation
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.
🦋 Changeset detectedLatest commit: 54934d0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
Verified on this branch at
Also ran |
_settleCancelledRequest returned before the bookkeeping cleanup when the per-request SSE stream had already been closed or dropped. In a batch where an answered sibling was parked in _requestResponseMap waiting on the cancelled id, that left both of its mappings behind - the same leak this PR fixes, on the disconnected path. send() already retires relatedIds when the stream is missing; do the same here and skip only the resolveJson/cleanup part. Adds a test that closes the stream from the first tool, cancels the second, and asserts both maps are empty and the answered response is still in the event store.
|
Thread resolved. Merged main into the branch to clear the behind state; full CI is green on 54934d0 (14 checks, 0 failures), including server-conformance and both e2e suites. |
Fixes #3053
After
notifications/cancelled,WebStandardStreamableHTTPServerTransportnever settles the original POST.Reproduction on
mainMcpServerwith one tool that waits for its request to be aborted,enableJsonResponse: true, POSTtools/call, then cancel 50ms later:Cause
Protocol._oncancelaborts the handler's controller._onrequestthen drops both the result and the error for that id when the signal is aborted, sosend()never runs for it. The transport only clears_requestToStreamMapping, resolvesresolveJsonand closes the per-request SSE stream fromsend(). 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
Transportinterface. Cancellation reaches the transport throughonmessage, so the transport can settle the request itself when it seesnotifications/cancelled._deliverMessagewrapper callsonmessage, then settles the cancelled request.onmessagedispatch sites inhandlePostRequestroute through it, so a cancel sent in the same batch as its request is covered._settleCancelledRequestretires 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:
and the SSE variant ends the stream with
read done=true bytes=0.Tests
Three cases in
packages/server/test/server/streamableHttp.test.ts:All three fail on
main(each times out waiting for a POST that never settles) and pass with the change. The wholepackages/serversuite is green: 55 files, 677 tests. Typecheck, ESLint and Prettier are clean.No new dependencies.