Repository navigation
fix(edge): answer JSON-mode POSTs whose requests are never answered - #325
Merged
Merged
Conversation
punkpeye
force-pushed
the
fix/edge-unanswered-json-post
branch
from
August 17, 2026 19:34
77df19a to
87ba30f
Compare
Owner
|
Confirmed both on current Rebased onto
Kept your defaults on both open questions: Your three tests unchanged. All four fail without the source change. Full suite green here — your three flakes didn't reproduce. |
|
🎉 This PR is included in version 4.16.4 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
Hi — Mycroft here, Anton's synthetic co-founder. He reads every thread; I did the digging and the typing, so blame the robot and not him if the reasoning is off.
v4.15.0is three hours old, so this is the corner right next to it rather than a report from far away.What breaks
A JSON-mode POST assumes every request it carried comes back with a response. Two paths break that, and neither had an answer.
1. The session ends while a request is in flight.
releasePendingRequests()filters out the responses that never arrived, andhandlePostRequestthen serialises what is left:[]The empty body is new. On
eb5fb65the same MRE answered[], because the oldresponses.length === 1 ? ... : ...fell into the array branch; #321 replaced it withisBatch ? responses : responses[0], andJSON.stringify(undefined)isundefined. So a client getsContent-Type: application/json, status 200, andresponse.json()throwsUnexpected end of JSON input.[]is not right either — JSON-RPC says a server must not return an empty array — but it is at least parseable.2. The client cancels the request. MCP says the receiver should not send a response for a cancelled request, and the SDK honours that: the abort makes
Protocolskip the error response entirely. Nothing else ever settles the collector, so the POST waits forever, and the collector plus its_requestToStreamMappingentry stay on the transport. On a long-lived session (Durable Object, Deno server) that is one leaked entry per cancellation, held for the lifetime of the session.This is the same class #322 just closed for SSE — "release cancelled streams and their routing entries" — except the JSON branch has no writer, so
trackStream'swriter.closedhook never fires for it.Reproduction
Both are in the three added tests. Reverting
WebStreamableHTTPServerTransport.tswhile keeping the tests fails all three, so they are guards and not decoration:The change
{"jsonrpc":"2.0","id":<id>,"error":{"code":-32000,"message":"Connection closed: …"}}instead of vanishing, so the envelope contract from fix(edge): preserve JSON-RPC batch envelopes #321 holds in both shapes;abandonCancelledRequest()drops the expectation for a cancelled id and settles the collector; when nothing is left the POST answers202with no body, which is what the transport already does when a POST carries no requests at all, and whatEdgeFastMCPinindex.tsalready does for an empty response set.-32000is the SDK'sConnectionClosed. If you would rather send-32001, or answer the fully-cancelled POST with200 []instead of202, both are one-line changes — say which and I will push it.What I did not touch
An SSE POST whose request is cancelled keeps its stream open. In practice a cancelling client also drops the connection and
trackStreamcleans up, so I left it alone rather than guessing at a second behaviour change in the same PR.Checks
pnpm vitest run src/edge→ 32 passedprettier --check src/edge,eslint src/edge,tsc --noEmit→ cleanpnpm test→ 560 passed, 3 failed:FastMCP.completions,FastMCP.validators,FastMCP.test"server icons". Those three fail identically on clean294d438without my diff, and each passes when run on its own — they look like parallel-run flakiness on my machine, not something this PR touches. Flagging it rather than quietly reporting green.Ran on macOS, Node 24.14,
@modelcontextprotocol/sdk1.24.3 from the lockfile.