Skip to content

fix: treat a CRLF split across SSE chunks as one line ending - #6

Open
mrgoonie wants to merge 4 commits into
fix/2-sse-buffer-capfrom
fix/5-sse-crlf
Open

mrgoonie wants to merge 4 commits into
fix/2-sse-buffer-capfrom
fix/5-sse-crlf

Conversation

@mrgoonie

Copy link
Copy Markdown
Contributor

Stacked on #4 (fix/2-sse-buffer-cap), which is stacked on #3. Review and merge those first; this PR's diff is only the CRLF fix.

Problem

SseParser.push treated a \r at the end of one chunk as a line ending and then read a \n opening the next chunk as a second, empty line. An empty line dispatches the event, so an event with only event: set could be sent early with no data, and its real data arrived as a separate event.

Fix (src/protocol/sse-reader.ts)

  • New endedWithCr flag: set when a chunk ends with \r.
  • When the next chunk starts with \n and the flag is set, parsing starts at index 1, so the split CRLF counts as one line ending (WHATWG event-stream format).
  • Empty chunks return early and keep the flag.
  • The 1 Mi buffer cap and its accounting, the failed state and the end-of-stream flush are unchanged. The line regex \r\n|\r|\n always consumes a trailing \r, so the buffer never holds one.

Tests (tests/unit/protocol/sse-reader.test.ts)

  • push('event: x\r') then push('\ndata: y\r\n\r\n') gives exactly one event x with data y.
  • Lone CR: push('data: a\rdata: b\r\r') gives one event with data a\nb.
  • readSseFrames over 'event: x\r' + '\ndata: y\r', then the stream ends, gives one event x/y.
  • Mutation check: with start set back to 0, the split-CRLF parser test and the stream test both fail (2 failed, 13 passed in the file). With the fix restored, all pass.

Gates

  • pnpm lint: pass
  • pnpm typecheck: pass
  • pnpm test: 27 files, 390 tests pass
  • pnpm build: pass
  • pnpm test:e2e (no .env): 5 smoke tests pass, 16 live tests skipped

Closes #5

🤖 Generated with Claude Code

@mrgoonie mrgoonie self-assigned this Oct 12, 2026
mrgoonie and others added 3 commits October 12, 2026 09:14
A `\r` at the end of one chunk ended the line, and a `\n` opening the
next chunk was then read as a second, empty line, which dispatched the
event early. The parser now remembers a trailing `\r` and skips a `\n`
that immediately follows it in the next chunk, as the WHATWG
event-stream format requires. Lone `\r` line endings still work, and the
buffer cap, failed state and end-of-stream flush are unchanged.

Closes #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin three cases: an empty chunk between the CR and LF of a split CRLF,
an LF that opens the next chunk (one frame, not two), and CR CR followed
by LF (one frame, nothing more). Note why the empty-chunk return must
come before the endedWithCr update.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The LF that ends a CRLF split across chunks does not count toward the
next line, so the cap still applies exactly, and the frame that LF
completes is kept when the rest of the chunk passes the cap.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant