fix(server): settle pending JSON-mode responses when the streamable HTTP transport closes - #2692
Open
retif wants to merge 1 commit into
Open
Conversation
🦋 Changeset detectedLatest commit: ba3b959 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: |
…TTP transport closes In JSON response mode, handleRequest() returns a Promise<Response> that only send() resolves. A stream mapping's cleanup deletes the entry without settling that promise, so close() while a POST was in flight left the HTTP request open until the socket died, with the caller unable to tell whether the request ran. close() now resolves any pending JSON-mode response before the stream mappings are torn down, with a JSON-RPC error (-32000, "Connection closed") per outstanding request id; an id in a batch that already has a real response reuses it. The SSE path is left as documented by hosting:session:delete-cancels-inflight — a POST-initiated stream closing without a response is cancellation, not a lost reply.
retif
force-pushed
the
fix/close-answers-inflight-requests
branch
from
August 21, 2026 00:39
916844b to
ba3b959
Compare
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 the
close()half of #2559.Problem
In JSON response mode,
handleRequest()returns aPromise<Response>that onlysend()resolves. A stream mapping'scleanupdeletes the entry without settling that promise, soWebStandardStreamableHTTPServerTransport.close()while a POST is in flight leaves the HTTP request open until the socket dies.This is #2559's second paragraph — "
cleanuponly deletes the entry without settling the promise, soclose()during an in-flight JSON-mode POST leaves the HTTP request hanging" — reported there as pre-existing on bothmainandv1.x.The caller cannot distinguish "never ran" from "ran, reply lost", so for a mutating call retrying may double-execute and giving up may drop a completed write. In practice it presents as a client hanging until its own idle timeout on a call the server has already stopped working on.
Change
settlePendingJsonResponses()runs inclose()before thecleanup()loop, sincecleanup()drops the mapping that holdsresolveJson.enableJsonResponseis set and something is in flight.send()already produces.-32000 "Connection closed: the server transport closed before this request completed". An id that already has a real response (a batch sibling that completed first) reuses it rather than being overwritten.close()from completing.close()also clears_requestToStreamMapping, which it previously left populated.NodeStreamableHTTPServerTransportwraps this transport, so it inherits the fix.Scope
close()only, and only the JSON-response-mode path.This does not address #2559's first half — JSON-mode mappings leaking one
_streamMappingentry per completed POST — so that issue should stay open.Tests
Three tests in
packages/server/test/server/streamableHttp.test.tsunderclose() with a JSON-mode request in flight.packages/servertest/e2ehosting:session:delete-cancels-inflight, which this version leaves intactNegative control, run against
3924de9's copy of the transport with the new tests kept: both behavioural tests hang to the 5 s test timeout, which is the defect itself. The third (is a no-op when nothing is in flight) passes either way by design.tsgo -p packages/server/tsconfig.json --noEmitis clean. A changeset is included.Not verified here
eslint/prettierwere not run locally (tooling constraint on my machine). The added lines follow.prettierrcby hand — 4-space, single quotes,arrowParens: avoid, nothing over 140 columns — but CI is the authority.The v1 line has the same defect and is not touched here. Happy to open the
v1.xbackport if you want it.