{{ message }}
Repository navigation
Conversation
The SSE reader treated each network chunk as if it were a unit of the protocol. Three things go wrong with a server that is merely following the format. A chunk can end in the middle of a multi-byte character. `decoder.decode(value)` without `stream: true` decodes that half on its own and emits U+FFFD, so any non-ASCII payload the server split across writes arrives corrupted - and a JSON body corrupted that way either fails to parse or parses with mojibake inside it. Events are separated by a BLANK LINE, and the line terminator may be CRLF, LF or CR. The reader searched for `"\n\n"` only, which a CRLF stream never contains, so every event stayed in the buffer and the caller waited out its timeout against a server that was sending correctly. One event may carry several `data:` lines, and they are one payload joined by newlines - which is how a server that pretty-prints its JSON sends it. Each line overwrote the last, so `JSON.parse` received a fragment. The framing moves into an exported `parseSseStream(chunks)` generator so it can be tested without a socket: `connectSse` feeds it the fetch reader, and the tests feed it byte chunks split at the boundaries that used to break it.
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.

Sibling of #286, same class on the other transport: the reader treats a network chunk as a unit of the protocol. Three things go wrong here with a server that is merely following the event-stream format.
A chunk can end in the middle of a character.
decoder.decode(value)withoutstream: truedecodes that half on its own and emits U+FFFD. Any non-ASCII payload the server split across writes arrives corrupted — and a JSON body corrupted that way either fails to parse or parses with mojibake inside it. Chinese tool names and descriptions are exactly the length where this shows up.Events are separated by a blank line, and the line terminator may be CRLF, LF or CR. The reader searched for
"\n\n", which a CRLF stream never contains —\r\n\r\nhas no\n\nin it. Against a server sending CRLF, every event stayed in the buffer and the call waited out its 15s timeout while the data sat there.One event may carry several
data:lines, and they are one payload joined by newlines — which is how a server that pretty-prints its JSON sends it. Each line overwrote the last, soJSON.parsereceived the closing brace of the body.The change
decode(value, { stream: true }), a separator that accepts all three line terminators, anddata:lines collected instead of overwritten. The framing moves into an exportedparseSseStream(chunks)generator, which is what makes it testable:connectSsefeeds it the fetch reader, and the tests feed it byte chunks.parseChunkkeeps its shape —id,eventand any other field are unchanged, and a single-line event produces exactly what it produced before.Reading ends the same way: a trailing partial event is not an event, and is dropped rather than guessed at.
Tests
packages/eko-core/test/mcp-sse-parsing.test.ts, seven cases over the real decoder and no socket:CRLF-separated events; CR-separated events; a pretty-printed body across several
data:lines; a multi-byte character split at its lead byte (asserting the text round-trips and no U+FFFD survives); an event split at three arbitrary points; an ordinary LF stream with a trailing partial event dropped; andid/retryfields preserved.Checked against mutants rather than only against the fix — each piece is graded by the test written for it: dropping
stream: truefails only the split-character case, narrowing the separator to\n\nfails the CRLF and CR cases, keeping only the lastdata:line fails the multi-line case, and splitting lines on\nalone fails the CR case.tsc -p tsconfig.test.jsonreports nothing new (src/agent/agent-llm.tshas a pre-existing AI-SDK typing error onmain, untouched here). Tested on macOS, Node 24.This branch is independent of #286 — different package, no shared files — so the two can land in either order.