fix(streams): frame multiline SSE payloads as consecutive data: lines - #393
Conversation
… instead of stripping newlines ## Summary Envelope.message collapsed \r/\n in broadcast payloads to nothing before writing a single data: line — silent corruption of whitespace-significant content on both ephemeral and durable delivery. Payloads are now split on SSE line terminators into consecutive data: lines, which EventSource rejoins with \n (lossless, trailing newline included). Injection defense preserved: every payload line is data:-prefixed; event names and comments still strip newlines. The stream-source element's fetch-path parser had the matching client bug (joined without \n, trim()ed payload) and now follows EventSource semantics. ## Test Coverage - exact multi-line framing of a multiline payload - EventSource-style rejoin round-trip (incl. blank lines, trailing \n) - \r\n / lone \r normalization; no raw \r in frames - SSE field-injection impossible via payload newlines - empty payload still emits its data: line ## Verification - [x] bundle exec rubocop passes - [x] bundle exec rspec spec/pgbus/ spec/generators/ — 3826 examples, 0 failures Refs #392
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe change preserves multiline SSE payloads. The server emits one ChangesSSE multiline payload handling
Estimated code review effort: 2 (Simple) | ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 5: Update the CHANGELOG entry’s inline code formatting around the SSE
data prefix: remove the trailing space from the `data:` code span and describe
the separator space outside the span, preserving the existing explanation.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8f5a17c6-35cd-46a2-8f09-9afa6723be19
📒 Files selected for processing (4)
CHANGELOG.mdapp/assets/javascripts/pgbus/stream_source_element.jslib/pgbus/streams/envelope.rbspec/pgbus/streams/envelope_spec.rb
Summary
Streams::Envelope.message(lib/pgbus/streams/envelope.rb) no longer strips\r/\nfrom payloads — a multiline payload is split on SSE line terminators (\r\n|\r|\n) and framed as consecutivedata:lines, which EventSource clients rejoin with\n. Delivery is lossless, including a trailing newline (empty finaldata:line, viasplit(re, -1)).<pgbus-stream-source>fetch-path parser (app/assets/javascripts/pgbus/stream_source_element.js) had the matching client-side bug:data += line.slice(5).trim()joined multi-line frames without\nand trimmed significant whitespace. It now follows EventSource semantics — join with\n, strip only the single leading space after the colon. (The native-EventSource reconnect path andspec/support/sse_test_client.rbwere already spec-compliant.)data:prefix, so a crafted payload cannot forgeid:/event:fields (spec proves it). Single-line fields — event names, comments — still strip newlines, where a newline is pure injection.Both the ephemeral and durable delivery paths go through
Connection#enqueue→Envelope.message, so the durable path flagged in the issue is fixed by the same change.Closes #392
Test plan
bundle exec rspec spec/pgbus/streams/envelope_spec.rb— framing, round-trip,\rnormalization, trailing newline, injection, empty payload (25 examples)bundle exec rspec spec/pgbus/ spec/generators/— CI unit scope, 3826 examples, 0 failures, coverage 90.57% line / 78.13% branch"<div>\n <b>multi</b>\n <i>line</i>\n</div>"on an ephemeral stream, observedata:line per payload line on the wire and byte-identical payload client-sideDeviations & judgment calls
data:framing satisfies both; an explicit injection spec proves every payload line staysdata:-prefixed.\n+trim()); fixed to EventSource semantics. Without this the server fix alone would leave first-connect (XHR) delivery corrupted.\r\nand lone\rnormalize to\non rejoin — SSE has no representation for a raw\r(it is a line terminator in the spec); this is the maximum fidelity SSE permits. Documented in the module docs.data:line — Ruby's"".split(re, -1)returns[], which would have dropped the data field entirely; guarded and spec'd.node --checkand conformance reasoning against the WHATWG SSE parsing algorithm.Summary by CodeRabbit
Bug Fixes
Tests