fix(client): seed SSE resumption tracker from resumptionToken (#2499)#2509
Open
vishnujayvel wants to merge 1 commit into
Open
Conversation
A resumed stream (Last-Event-ID: e1) that disconnects again before any new id-bearing event arrives (LB idle timeout, server restart) was reconnecting with no Last-Event-ID header at all, because _handleSseStream() tracked the latest event id in a local variable that always started undefined instead of being seeded from the resumptionToken the stream was (re)opened with. The server then treated the reconnect as a brand-new stream instead of a resumption. Seed the tracker from options.resumptionToken so a reconnect that saw no new events re-sends the same token. Fixes modelcontextprotocol#2499
🦋 Changeset detectedLatest commit: f18c728 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: |
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.
Seed
_handleSseStream()'s resumption-token tracker from theresumptionTokena stream was (re)opened with, so a resumed stream that disconnects again before any new event arrives still reconnects with the sameLast-Event-ID.Motivation and Context
StreamableHTTPClientTransport._handleSseStream()tracks the latest event id in a locallastEventIdthat always startsundefined. It is never seeded from theresumptionTokenthe stream was opened with, even though both reconnect paths (onDone/onError) passresumptionToken: lastEventIdwhen scheduling the next attempt.So when a stream resumed with
Last-Event-ID: e1disconnects again before any id-bearing event arrives — a load-balancer idle timeout or a server restart, both plausible on a long-running request — the scheduled reconnect GET carries noLast-Event-IDheader at all. The server has no way to know this was meant to be a resumption, treats it as a brand-new standalone stream, and the events the client was waiting to replay are never sent; the original request just hangs until timeout.The fix seeds the tracker from
options.resumptionTokenat the top of_handleSseStream(). Since replay is idempotent (that's the whole point ofLast-Event-ID), re-sending the same token when no new events arrived is exactly the correct behavior — it's what the client would have sent had it never gotten this far in the first place.This only affects the reconnect path: the initial POST send always constructs a fresh
optionsobject with noresumptionToken, solastEventIdthere is still seededundefinedas before — no behavior change outside of resumed streams.How Has This Been Tested?
One new unit test in
packages/client/test/client/streamableHttp.test.ts(SSE retry field handlingdescribe block, alongside the existing "should reconnect on graceful stream close" test it mirrors): opens a stream via_startOrAuthSse({ resumptionToken: 'event-1' }), mocks the resumed GET's body closing immediately with no events, and asserts the second fetch call's headers carrylast-event-id: event-1(previouslynull).Revert-proofed by reverting the fix in isolation: the new test fails with
expected null to be 'event-1', confirming it actually exercises the bug; restoring the fix makes it pass again.Full
@modelcontextprotocol/clientsuite: 739/739 pass (738 pre-existing + 1 new), no pre-existing failures.pnpm --filter @modelcontextprotocol/client typecheckandlint(eslint + prettier) both clean.Breaking Changes
None. This only changes the
Last-Event-IDheader value sent on a reconnect GET that would otherwise have omitted it; no public API or type signature changes.Types of changes
Checklist
Additional context
Changeset included (
@modelcontextprotocol/client: patch).Credit to @ayush00git for the report, root-cause analysis, and suggested fix in #2499 — this PR implements exactly the direction proposed there. @jspahrsummers and @harshmathurx also weighed in on the issue confirming the approach.