fix(adapters): incremental byte-level SSE parser (FWD-06)

Replace per-chunk parse_sse_frames with SseParser holding raw bytes
across chunks: reassembles frames split at TCP boundaries (the review's
silently-losing case), decodes UTF-8 per complete line so multi-byte
chars split across chunks survive, caps the buffer at 1 MiB
(SSE_EVENT_BUFFER_CAP) and dispatches a pending event at EOF.

forward_stream threads the parser through its unfold state and emits a
terminal error envelope on cap overflow. Existing single-chunk SSE test
assertions preserved; added multi-chunk, split-UTF-8, EOF-dispatch, and
cap tests.

Verified: cargo test (219 pass), clippy -D warnings, fmt --check.
This commit is contained in:
2026-08-29 08:35:33 +00:00
parent e4284a0d3c
commit cc34c08e4e
3 changed files with 314 additions and 75 deletions
+92 -10
View File
@@ -801,43 +801,125 @@ mod tests {
#[test]
fn sse_frames_parse_multi_event_buffer() {
let (events, remaining) =
crate::adapters::forward::parse_sse_frames("data: a\n\ndata: b\n\n");
let mut parser = crate::adapters::forward::SseParser::new();
let events = parser
.feed(b"data: a\n\ndata: b\n\n", false)
.expect("parse errors impossible on ascii");
assert_eq!(events.len(), 2);
assert_eq!(events[0].data, "a");
assert_eq!(events[1].data, "b");
assert_eq!(remaining, "");
let tail = parser.feed(b"", false).expect("tail");
assert!(tail.is_empty());
}
#[test]
fn sse_frames_handle_partial_trailing_line() {
let (events, remaining) =
crate::adapters::forward::parse_sse_frames("data: a\n\ndata: par");
let mut parser = crate::adapters::forward::SseParser::new();
let events = parser
.feed(b"data: a\n\ndata: par", false)
.expect("parse errors impossible on ascii");
assert_eq!(events.len(), 1);
assert_eq!(remaining, "data: par");
let rest = parser.feed(b"tial\n\n", false).expect("rest");
assert_eq!(rest.len(), 1);
assert_eq!(rest[0].data, "partial");
}
#[test]
fn sse_frames_skip_comment_lines() {
let (events, _) = crate::adapters::forward::parse_sse_frames(": comment\ndata: x\n\n");
let mut parser = crate::adapters::forward::SseParser::new();
let events = parser
.feed(b": comment\ndata: x\n\n", false)
.expect("parse errors impossible on ascii");
assert_eq!(events.len(), 1);
assert_eq!(events[0].data, "x");
}
#[test]
fn sse_frames_join_multi_line_data() {
let (events, _) =
crate::adapters::forward::parse_sse_frames("data: line1\ndata: line2\n\n");
let mut parser = crate::adapters::forward::SseParser::new();
let events = parser
.feed(b"data: line1\ndata: line2\n\n", false)
.expect("parse errors impossible on ascii");
assert_eq!(events.len(), 1);
assert_eq!(events[0].data, "line1\nline2");
}
#[test]
fn parse_sse_frames_strips_bom() {
let (events, _) = crate::adapters::forward::parse_sse_frames("\u{feff}data: a\n\n");
let mut parser = crate::adapters::forward::SseParser::new();
let events = parser
.feed("\u{feff}data: a\n\n".as_bytes(), false)
.expect("parse errors impossible on bom+ascii");
assert_eq!(events.len(), 1);
}
#[test]
fn sse_multichunk_events_reassembled() {
let mut parser = crate::adapters::forward::SseParser::new();
let first = parser
.feed(b"data: {\"n\":1}\n", false)
.expect("parse errors impossible on ascii");
assert!(first.is_empty(), "no blank line yet, event pending");
let second = parser
.feed(b"\ndata: {\"n\":2}\n\n", false)
.expect("parse errors impossible on ascii");
assert_eq!(
second.len(),
2,
"the review's empirically-verified loss case"
);
assert_eq!(second[0].data, "{\"n\":1}");
assert_eq!(second[1].data, "{\"n\":2}");
let eof = parser.feed(b"", true).expect("eof");
assert!(eof.is_empty(), "no event left pending");
}
#[test]
fn sse_multichunk_split_utf8_char() {
let payload = "{\"s\":\"héllo\"}";
let bytes = format!("data: {payload}\n\n").into_bytes();
let split = bytes.len() - payload.len() + 3;
assert!(
payload.as_bytes()[split - 8..].contains(&0xc3),
"split inside multi-byte char"
);
let (head, tail) = bytes.split_at(split);
let head = head.to_vec();
let tail = tail.to_vec();
let mut parser = crate::adapters::forward::SseParser::new();
let first = parser.feed(&head, false).expect("first chunk");
assert!(
first.is_empty(),
"frame incomplete until blank line arrives"
);
let second = parser.feed(&tail, false).expect("second chunk");
assert_eq!(second.len(), 1);
assert_eq!(second[0].data, payload);
}
#[test]
fn sse_pending_event_dispatched_at_eof() {
let mut parser = crate::adapters::forward::SseParser::new();
let pending = parser.feed(b"data: tail-event\n", true).expect("eof feed");
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].data, "tail-event");
}
#[test]
fn sse_oversized_partial_line_no_dispatch() {
let big_line = "x".repeat(16 * 1024 * 1024);
let body = format!("data: {big_line}");
let mut parser = crate::adapters::forward::SseParser::new();
let result = parser.feed(body.as_bytes(), false);
assert!(
matches!(
result,
Err(crate::adapters::forward::SseParseError::BufferOverflow)
),
"unterminated oversized line must trip the cap"
);
}
#[test]
fn http_service_config_struct_fields() {
let cfg = config(