LLM client reads an unbounded response body before checking the status #53

Closed
opened 2026-10-03 18:48:25 +00:00 by mathias · 1 comment
Owner

Milestone v0.32.0. From #46 item 6, reproduced by the phase-1 investigation. internal/adapters/llm/client.go runs io.ReadAll(resp.Body) before the status check, so a misbehaving gateway or proxy that streams a huge body is bounded only by the client's overall HTTP timeout; this client serves the summarizer, chat and the reconciliation adjudicator. (The error TEXT is already bounded to 512 bytes since v0.31.0; the read is not.)

Fix: wrap the body in io.LimitReader (about 8 MiB for a success body, and read at most maxErrorBody when the status is not 200). Red test first: TestClient_ABodyLargerThanTheCapIsAnErrorNotAnAllocation (a streaming server returning more than the cap gives an error and the client never holds more than the cap). Acceptance: red first, then green; task check and golangci-lint run ./... exit 0.


Created via git-mcp on behalf of @mathias

**Milestone v0.32.0. From #46 item 6, reproduced by the phase-1 investigation.** `internal/adapters/llm/client.go` runs `io.ReadAll(resp.Body)` before the status check, so a misbehaving gateway or proxy that streams a huge body is bounded only by the client's overall HTTP timeout; this client serves the summarizer, chat and the reconciliation adjudicator. (The error TEXT is already bounded to 512 bytes since v0.31.0; the read is not.) **Fix:** wrap the body in `io.LimitReader` (about 8 MiB for a success body, and read at most `maxErrorBody` when the status is not 200). **Red test first:** `TestClient_ABodyLargerThanTheCapIsAnErrorNotAnAllocation` (a streaming server returning more than the cap gives an error and the client never holds more than the cap). **Acceptance:** red first, then green; `task check` and `golangci-lint run ./...` exit 0. --- _Created via git-mcp on behalf of @mathias_
mathias added this to the v0.32.0 - audio upload follow-ups milestone 2026-10-03 18:48:25 +00:00
mathias added the risk/lowsize/xstype/fix labels 2026-10-03 18:48:25 +00:00
Author
Owner

Shipped in v0.32.0 (PR #58, merge 85f4cc3; running in production since 2026-10-04, pod 1/1, 0 restarts, now image 7b7da65). The LLM client reads a success body up to 8 MiB (defaultMaxResponseBytes) and errors with "LLM response exceeds N bytes" beyond it; a failing response is read only up to 513 bytes.

Evidence: red tests first (bodycap_test.go), then pinned at the boundary by the adversarial review: body of cap-1 and exactly cap accepted, cap+1 refused, the production default (client built with no option) at exactly 8 MiB passes and 8 MiB + 1 is refused, a non-positive override keeps the default. Mutations that went red: >→>=, a 1 TiB default, 4 MiB and 16 MiB defaults, n > 0→n > -1. Reviewer also executed a gzip bomb (411 KB compressed, ~200 MiB decompressed): rejected in 0.2 ms, the cap applies after decompression, and the 429-then-200 retry still works.

Not observed live: there is no way to make the production gateway stream an oversized body, so the behaviour rests on the tests and the review, not on a live trigger. Closing on that basis.


Created via git-mcp on behalf of @mathias

**Shipped in v0.32.0** (PR #58, merge 85f4cc3; running in production since 2026-10-04, pod 1/1, 0 restarts, now image 7b7da65). The LLM client reads a success body up to 8 MiB (`defaultMaxResponseBytes`) and errors with "LLM response exceeds N bytes" beyond it; a failing response is read only up to 513 bytes. Evidence: red tests first (`bodycap_test.go`), then pinned at the boundary by the adversarial review: body of cap-1 and exactly cap accepted, cap+1 refused, the production default (client built with no option) at exactly 8 MiB passes and 8 MiB + 1 is refused, a non-positive override keeps the default. Mutations that went red: `>`→`>=`, a 1 TiB default, 4 MiB and 16 MiB defaults, `n > 0`→`n > -1`. Reviewer also executed a gzip bomb (411 KB compressed, ~200 MiB decompressed): rejected in 0.2 ms, the cap applies after decompression, and the 429-then-200 retry still works. Not observed live: there is no way to make the production gateway stream an oversized body, so the behaviour rests on the tests and the review, not on a live trigger. Closing on that basis. --- _Created via git-mcp on behalf of @mathias_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/tapir#53