fix(core): ignore extra blank lines in event stream decoder - #111
Conversation
The decoder treated exactly two line endings as a message delimiter, so an
extra blank line was kept as pending data. A stream ending in '\n\n\n' made
end() throw "Event Stream ended before complete", and consecutive delimiters
emitted a spurious {} event. The spec treats extra blank lines as no-ops.
A run of blank lines is now one delimiter, and line endings at a message
boundary are dropped, which also covers a CRLF split after a delimiter. The
delimiter scan uses a single exec loop instead of test() plus matchAll().
@standard-server/aws-lambda
@standard-server/core
@standard-server/fastify
@standard-server/fetch
@standard-server/node
@standard-server/peer
@standard-server/shared
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will improve performance by 27.58%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | large message 100KB in one chunk |
848.4 µs | 665 µs | +27.58% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/event-stream-decoder-blank-lines-ad4cf7 (1439bb1) with main (90876d0)
Footnotes
-
108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
There was a problem hiding this comment.
ℹ️ No critical issues — one small test-naming nit inline.
Reviewed changes
- Greedy message delimiter —
MESSAGE_DELIMITER_REGEXnow requires 2+ line endings, so runs of blank lines collapse into a single delimiter and no longer emit a spurious empty{}event. - Leading blank-line stripping — when
pendingis empty,feed()drops a leading[\r\n]+run, replacing the removeddiscardLeadingLFstate. - Overlap window reduced —
MAX_DELIMITER_OVERLAP3 → 2, justified becausependingnever contains a blank line. - Single-pass scan — the global regex is driven via
exec/lastIndexinstead oftest+matchAll. - Tests — split-position and chunk-size tests extended with leading/repeated/trailing blank lines; a new end-to-end
toAsyncIteratorObjectcase reproduces the reported failure.
I re-ran the changed tests both ways: they fail on the base decoder (90876d0) and pass on this head (71665f7); the core decoder and fetch event-stream suites are green (50 tests).
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
Adds tests for streams of only blank lines, comment-only messages among extra blank lines, and a truncated message after extra blank lines, which must still throw on end(). Renames the empty-chunk test to describe what it now checks, per review.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review (71665f7), the branch added one test-only commit (1439bb1) that broadens coverage of the blank-line handling fixed earlier:
- Blank-line-only streams — new case asserts a stream of only
\n/\r/\r\nblank lines emits nothing and does not throw onend(). - Comment-only messages preserved — new case pins that comment-only messages (keep-alives) surrounded by extra blank lines are still emitted, so blank-line skipping cannot swallow them.
- Incomplete message after blanks — new case asserts
end()still throws when extra blank lines are followed by an incomplete message. - End-to-end fetch coverage — new
toAsyncIteratorObjectcase feeds leading, repeated and trailing blank lines plus comment pings, and expects clean message delivery followed by normal completion. - Chunk-boundary tests extended — the split-position matrix now includes multi-ending delimiters and a leading delimiter; the chunk-size parity stream carries leading/extra/trailing blank lines; the empty-chunk test was renamed to describe what it actually covers.
I re-ran the changed tests both ways: reverting decoder.ts to the base (90876d0) makes the four new behavioral cases fail (spurious {} events and EventStream ended before complete), and they pass on the head (1439bb1). Both suites are green (53 tests).
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Event streams that contain extra blank lines now decode cleanly. Previously a stream ending with an extra newline made the decoder throw "Event Stream ended before complete" after delivering every event, so
toAsyncIteratorObjectconsumers saw anEventStreamDecoderErrorinstead of normal completion. The spec treats extra blank lines as no-ops, so any producer or proxy that sends one triggered this.Fixes
\n,\ror\r\n) no longer throw on end{}eventPerformance
Testing
maintoAsyncIteratorObjecttest covering the reported failure