[triage:tooling-02-sse-endpoints-buffered-with-content-length] SSE / streaming endpoints return a single buffered body with `Content-Length` instead of streaming
- Dominant language
- Rust
- Stars
- 72
- Forks
- 13
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 5
Description
Imported from Capsem triage report `tooling-02-sse-endpoints-buffered-with-content-length.md`.
- Severity: `medium`
- Category: `bug (mock divergence)`
- Area: `capsem-mock-server`
- Location: `crates/capsem-mock-server/src/main.rs:339-348` (`/sse/model`); also `/v1/chat/completions` stream (442), `/v1/responses` stream (461-465), `/v1/messages` stream (509), all `:streamGenerateContent` arms (554, 589-593)
- Confidence: `verified`
## Summary
Every `text/event-stream` response is built via `response()` / `json_response()` helpers that set an explicit `Content-Length` and wrap the payload in `http_body_util::Full` — a single, fully-buffered frame. Real SSE/model-streaming upstreams send `Transfer-Encoding: chunked` (no `Content-Length`) and flush events incrementally over time. The MITM proxy's SSE parser and incremental-telemetry path is therefore tested against a degenerate "all events arrive at once, with a Content-Length" stream that no real upstream produces.
## Evidence
`/sse/model` arm:
```rust
(&Method::GET, "/sse/model") => response(
StatusCode::OK,
Bytes::from_static(b"event: model.delta\ndata: {...}\n\n event: model.tool_call\n..."),
"text/event-stream",
),
```
`response()` → `response_builder()` always adds `CONTENT_LENGTH` (line 686) and `full()` returns `Full::new(body)` (line 689-691), i.e. one buffered frame. The same is true for `openai_chat_stream()` (807), `responses_stream()` (876), `anthropic_stream()` (946), `google_code_assist_stream()` (1018), `gemini_api_stream()` (1125): each returns a `Bytes` blob handed to `response(..., "text/event-stream")`. None use a streaming body type, none omit `Content-Length`, none insert delays between events.
## Impact
SSE parsers behave differently when events arrive in one TCP read versus split across reads/time. A proxy bug that mishandles an event split across read boundaries, or that relies on chunked framing / absence of `Content-Length` to detect stream end, would pass every test against this mock. This directly undermines `dev-mitm-proxy` SSE-parsing coverage and the `sse_model` benchmark scenario (which measures a buffered transfer, not a stream).
## Suggested fix
Provide a streaming body for `text/event-stream` responses: use a channel/`StreamBody` that yields each `event:`/`data:` block as a separate frame, omit `Content-Length`, and for the delayed variants insert small `sleep`s between frames. Keep the buffered helpers for non-streaming JSON only.
## Triage
Confirmed from the local reviewed report in `/Users/elie/git/capsem/tmp/bugs/tooling-02-sse-endpoints-buffered-with-content-length.md`. Track implementation in the triage sprint; add regression coverage before fixing.
Contributor guide
Research direction
Start in crates/capsem-mock-server/src/main.rs at the /sse/model arm and response_builder()/full(), then inspect openai_chat_stream(), responses_stream(), anthropic_stream(), google_code_assist_stream(), and gemini_api_stream(). Add regression coverage for incremental SSE delivery and verify that streaming responses omit Content-Length and no longer arrive as one buffered body.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100