Skip to content

test(node): read SSE streams until expected events arrive instead of assuming one fetch chunk - #2662

Open
claude[bot] wants to merge 3 commits into
mainfrom
fix/2661-node-sse-test-chunking
Open

test(node): read SSE streams until expected events arrive instead of assuming one fetch chunk#2662
claude[bot] wants to merge 3 commits into
mainfrom
fix/2661-node-sse-test-chunking

Conversation

@claude

@claude claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Requested by Felix Weinberger · Slack thread

Fixes #2661.

Problem

Two tests in packages/middleware/node/test/streamableHttp.test.ts call reader.read() once and then assert that multiple SSE events are present in that single chunk:

  • should handle batch request messages with SSE stream for responses — asserts both req-1 and req-2 responses after one read.
  • should store and replay MCP server tool notifications — asserts a notification after one read, twice (initial stream and the Last-Event-ID reconnect), even though the server writes a priming event (id:/retry: frame, packages/server/src/server/streamableHttp.ts) and the notification as separate writes.

fetch makes no guarantee that separately-written SSE events share a chunk. On Node 26.7 they arrive in separate reads, so both tests fail while the middleware itself behaves correctly (reported with a repro in #2661). On earlier Node versions the writes usually coalesce, which is why the tests have been green.

Fix (test-only)

Adds a readSSEUntil(response, predicate, timeoutMs = 2000) helper that accumulates decoded chunks until the predicate is satisfied, the stream ends, or the timeout cancels the reader — the same read-until pattern the neighboring should store and replay multiple notifications sent while client is disconnected test already uses — and applies it to the three single-read sites in the two affected tests. The helper releases the reader's lock on return so the disconnect-simulation cancel() in the replay test still works.

No production code is touched, so no changeset is included.

Verification

  • Code-verified the failure mode: each affected assertion depends on events from distinct server-side writes landing in one read().
  • pnpm --filter @modelcontextprotocol/node test -- test/streamableHttp.test.ts: 4 files, 99 tests pass (Node 22.22.2 — a Node 26.x runtime wasn't available in this environment; the loop is chunking-agnostic by construction, accumulating across reads until the expected text appears).
  • ESLint + Prettier + typecheck clean on the package.

Credit

The failure report, repro steps, and the read-until-predicate approach are from @jstar0 (#2661), who prepared an equivalent branch but hit a permissions error opening a PR. This PR independently implements that approach against main.


Generated by Claude Code

…assuming one fetch chunk

Two tests in packages/middleware/node/test/streamableHttp.test.ts called
reader.read() once and asserted that multiple SSE events were present in
that single chunk. fetch makes no such guarantee: on Node 26.7 the events
arrive in separate reads, so the tests fail while the middleware behaves
correctly.

Adds a readSSEUntil(response, predicate, timeoutMs) helper that
accumulates decoded chunks until the predicate is satisfied, the stream
ends, or the timeout cancels the reader — the same pattern the
neighboring "multiple notifications while disconnected" test already
uses — and applies it to the three single-read sites in the two
affected tests.

Fixes #2661

Approach credit: jstar0, who reported the failure and prepared an
equivalent branch but could not open a PR.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLGiVsm3WDL3j3Dnepy7Ci
@claude
claude Bot requested a review from a team as a code owner August 14, 2026 05:18
@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 3988a19

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 3988a19

@claude claude Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only fix that replaces single-chunk read() assumptions with a read-until-predicate loop, correctly addressing the Node 26.7 SSE chunking flakiness from #2661. The two inline comments are nits (a comment-wording inaccuracy and a suggested cleanup) and don't block merge.

Extended reasoning...

Overview

Single-file, test-only change to packages/middleware/node/test/streamableHttp.test.ts. It adds a readSSEUntil(response, predicate, timeoutMs) helper and applies it to the three sites in two tests that previously called reader.read() once and asserted on multiple SSE events landing in that single chunk — an assumption fetch never guaranteed and that breaks on Node 26.7. No production code, exports, or protocol behavior are touched, so the absence of a changeset is correct.

Security risks

None. The diff contains only test code — no auth, crypto, input handling, or network-facing production paths are modified.

Level of scrutiny

Low. This is test-infrastructure hardening following an established pattern already present in the same file (the neighboring disconnected-replay test uses the same read-until loop). The main correctness question is the helper's stream mechanics, which I verified: the timeout cancels the reader (making read() resolve done and breaking the loop rather than hanging), decoder.decode(value, { stream: true }) handles multi-byte splits, and releaseLock() in the finally block is what allows the replay test's subsequent sseResponse.body!.cancel() disconnect simulation to work — the diff consistently switched that call from reader.cancel() to body.cancel() to match.

Other factors

The bug hunting system's two findings are both nits: one comment misattributes chunk-splitting to a priming event that the standalone GET stream never writes (writePrimingEvent has a single call site in the POST handler), and one suggests replacing the surviving inline readWithTimeout copies (lines 1607 and 2403) with the new helper. Neither affects test correctness — the tests pass either way, and the helper is strictly more robust than the code it replaces. A candidate issue about a sibling single-read test was examined and ruled out by verifiers. The full package suite (99 tests) passes per the PR description, and the change reduces rather than adds flakiness risk.

Comment thread packages/middleware/node/test/streamableHttp.test.ts Outdated
Comment thread packages/middleware/node/test/streamableHttp.test.ts
…ming event

Review nit: the chunk-splitting note misattributed the split to a priming
event, but writePrimingEvent's only call site is the POST handler path.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLGiVsm3WDL3j3Dnepy7Ci

@claude claude Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only fix that replaces single-read SSE assertions with a read-until-predicate helper, correctly addressing the Node 26.7 chunking failures in #2661; the remaining inline comments are non-blocking nits. Thanks for already correcting the priming-event comment in 30a4cc6.

Extended reasoning...

Overview

Single-file, test-only change in packages/middleware/node/test/streamableHttp.test.ts. It adds a readSSEUntil(response, predicate, timeoutMs) helper that accumulates decoded chunks until the expected text appears, and applies it to the three single-read sites in the two tests that failed on Node 26.7 (#2661). No production code, no public API, no changeset needed.

Security risks

None. The diff touches only test code; no auth, crypto, network-facing, or production transport logic is modified.

Level of scrutiny

Low-to-moderate: the risk profile of a test-only PR is limited to masking real failures or introducing flakiness. I checked both directions: the helper still fails deterministically (timeout cancels the reader, assertions on the accumulated text then fail) if the middleware misbehaves, and it is chunking-agnostic by construction — decoder.decode(value, { stream: true }) handles multi-byte splits, the timeout is cleared and the reader lock released in finally, and the replay test's disconnect simulation was correctly updated from reader.cancel() to sseResponse.body!.cancel() since the helper releases its lock on return.

Other factors

All findings across both review runs are nits (stale prose in a doc note, a surviving inline copy of the read-until loop, and an unmigrated single-read in the pre-parsed batch test — currently green only due to handler-completion ordering). One prior nit (the priming-event comment misattribution) was already fixed in commit 30a4cc6. None of these block merge; the inline comments give the author drop-in fixes if they want to sweep them up. The PR's approach matches the pattern an adjacent test in the same file already used, and the package's full test suite passes per the PR description.

Comment thread packages/middleware/node/test/streamableHttp.test.ts
Comment thread packages/middleware/node/test/streamableHttp.test.ts
…o readSSEUntil

Review follow-up, three non-blocking nits in one sweep:
- the pre-parsed batch test still read one chunk over two
  separately-written batch responses (the #2661 pattern)
- the two surviving inline readWithTimeout closures now use the
  helper (one copy had drifted: fresh TextDecoder per chunk without
  { stream: true } and no releaseLock); the 5s bound is preserved
  via the timeoutMs parameter
- readSSEEvent's doc note now points at readSSEUntil instead of
  suggesting manual multi-reads

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLGiVsm3WDL3j3Dnepy7Ci

@claude claude Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only robustness fix, and the nits from earlier review rounds have all been addressed by the follow-up commits.

What was reviewed: the readSSEUntil helper (timeout cancels the reader, decode(..., {stream: true}), lock released in finally so the replay test's body.cancel() still works); the predicate choices at each migrated site (the batch test waits for both req-1 and req-2; the pre-parsed batch test's batch-1 predicate suffices since the tools payload arrives in the same frame); and the remaining single-read() sites in the file, which each await exactly one server-side write, so only intra-write segmentation risk remains — out of scope for #2661.

Extended reasoning...

Overview

PR #2662 changes a single test file, packages/middleware/node/test/streamableHttp.test.ts, to fix Node 26.7 test flakiness (#2661). It adds a readSSEUntil(response, predicate, timeoutMs) helper that accumulates decoded SSE chunks until a predicate matches, and migrates the single-read() sites and the two pre-existing inline readWithTimeout closures onto it. No production code is touched, so the absent changeset is correct.

Prior review rounds

Earlier runs of this review left four nit-level inline comments: (1) an inaccurate priming-event comment on the GET-stream test, (2) the surviving inline readWithTimeout closures, (3) a stale doc note on readSSEEvent, and (4) the unmigrated should handle pre-parsed batch messages single-read site. I verified in the current checkout (HEAD = 3988a19, the PR's latest commit, pushed after those comments) that all four are fixed: the comment now says the event "may arrive split across multiple chunks", both closures are folded into readSSEUntil, the doc note now points to readSSEUntil, and the pre-parsed batch test uses the helper. This run's bug hunting pass found no new issues.

Security risks

None. This is test code only; no auth, transport, or protocol logic changes. The helper is confined to the test file and not exported.

Level of scrutiny

Low-to-moderate: test-only change with no public API or runtime impact. The main risk in test-harness changes is masking real failures, and I checked for that: on timeout the helper cancels the reader, the loop exits on done, and the accumulated text is returned so the subsequent assertions still fail loudly — the helper cannot silently pass a test whose events never arrive. The replay test's disconnect simulation still works because the helper releases the reader lock before the test calls sseResponse.body!.cancel().

Other factors

The fix pattern matches what the neighboring replay tests already used, the approach originates from the reproduced issue report (#2661), and the author reports the full package test suite (99 tests) passing plus lint/typecheck clean. Remaining single-read sites in the file each await exactly one server-side write before reading, so they do not have the multi-write chunking bug this PR targets.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP SSE tests assume multiple events share one fetch chunk

1 participant