--- id: review-001-sse-parser name: Incremental byte-level SSE parser (FWD-06) status: completed depends_on: [] scope: narrow risk: high impact: component level: implementation tags: [adapters, review-001] --- ## Description Review 001 finding FWD-06 — silent subscription data loss, empirically verified: `parse_sse_frames` (`src/adapters/forward.rs:409-417`) keeps only the *last* line of each TCP chunk and discards any pending multi-line `data_buffer`, so a chunk ending exactly at `data: …\n` (blank line not yet arrived) silently loses the event. Verified: chunks `"data: {\"n\":1}\n"` + `"\ndata: {\"n\":2}\n\n"` yield only event 2. Single-chunk delivery (as in the tests) works, which is why the suite passes. Additional defects in the same parser: per-chunk `String::from_utf8_lossy` (`:357`) corrupts multi-byte characters split at a chunk boundary (JSON parse failure → event degraded to raw string); the trailing partial line has no length cap (unbounded buffering); an event pending at EOF is dropped (SSE says dispatch at EOF). For a subscription forwarder this is silent data loss with no error signal. Fix: an incremental **byte-level** parser carrying buffer state across chunks (decode UTF-8 once over the reassembled buffer, not per chunk). ## Acceptance Criteria - [x] Multi-chunk test: event split across two TCP chunks is delivered (the review's empirically-verified case — the acceptance gate) - [x] Split multi-byte UTF-8 across chunks parses (test) - [x] Pending event dispatched at EOF; trailing partial line length-capped (tests) - [x] Existing single-chunk SSE tests unchanged and green - [x] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass ## References - docs/reviews/001-initial-implementation-review.md (Part D, FWD-06; Part I, COV-01) ## Notes > Agent fills during implementation. Parser rewrite is isolated > from the response-decode fixes (content-type, size caps, error bodies) > in review-001-response-decoding so the delicate stateful rewrite lands > alone. Same file — sequence or coordinate. ## Summary > Filled on completion. **Completed** — FWD-06 fixed via an incremental byte-level SSE parser. ### What changed - `src/adapters/forward.rs`: replaced the per-chunk `parse_sse_frames` function with `SseParser`, a stateful byte-level parser. It carries the raw undecoded byte buffer across chunks, so a frame split at a TCP boundary reassembles and a multi-byte UTF-8 character split at a chunk boundary is no longer corrupted (UTF-8 is decoded per complete line, after reassembly, not per chunk). Framing kept to what the call protocol needs: lines split on `\n` with optional `\r`, `data:` accumulation joined with `\n` on dispatch, `event:`/`id:`/`retry:` accepted and ignored, comment lines skipped, blank line dispatches, leading BOM stripped, pending event with data dispatched at EOF. A documented `SSE_EVENT_BUFFER_CAP` (1 MiB) caps the reassembly buffer; exceeding it yields `SseParseError::BufferOverflow`, which `forward_stream` converts to a terminal error envelope instead of buffering without bound. - `forward_stream` now threads one `SseParser` through the `stream::unfold` state (plus a `broken` flag so a parse/transport error ends the stream) and dispatches the EOF-pending event when the upstream byte stream ends. - `src/adapters/from_openapi.rs`: the five existing single-chunk SSE test assertions were preserved (same expected event shapes) and ported to the new `feed` API; added the review's multi-chunk loss case (`"data: {\"n\":1}\n"` + `"\ndata: {\"n\":2}\n\n"` → both events), a split multi-byte UTF-8 test, an EOF-dispatch test, and an oversized-partial-line cap test. ### Verification - `cargo test` — 219 passed, 0 failed - `cargo clippy --all-targets -- -D warnings` — clean - `cargo fmt --check` — clean ### Notes for the sequential FWD-07/08/10/12 task - The parser rejects nothing on malformed UTF-8 inside a well-formed frame (invalid bytes in a `data:` value are ignored — no error); the only error is the buffer cap. If FWD-10/12 adds error-body handling, the `SseParseError` enum in forward.rs is the place to extend. - `SseParser::feed` is synchronous and pure; it holds no I/O, so the unfold-state shape `(bytes_stream, parser, broken)` can be reshaped freely without touching parser logic.