Skip to content

security: put a ceiling on the response body - #6

Open
simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:fix/limit-response-body-size
Open

simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:fix/limit-response-body-size

Conversation

@simonx1

@simonx1 simonx1 commented Sep 18, 2026

Copy link
Copy Markdown

Nothing bounded how much the client would read.

Net::HTTP buffers a whole response before handing it over, so a hostile endpoint, a proxy serving a runaway error page, or a compromised upstream could hand back gigabytes and the first code with an opinion about it was JSON.parse — by which time the bytes were already resident. Error bodies are worse: they're retained on the ApiError, so an oversized 500 body outlives the request.

The fix

max_response_bytes, 10 MiB by default, nil to disable. No decision response approaches that — the whole request is capped at 64k tokens and the answers are smaller than the questions — so the limit only ever bites on something that has gone wrong.

The default transport now reads in chunks and stops at the ceiling rather than collecting the body first, and an honest Content-Length over the limit ends it before a single chunk arrives. A body from a custom transport is measured on the way back, which cannot un-read it but does keep it out of the parser and off the exception.

ResponseTooLarge is a RubyDecisionModel::Error, so the retry loop passes it straight through: reading a too-large body again would only be slower. It carries #bytes and #limit.

Tests

test/response_size_test.rb, 9 cases. Three of them stand up a real loopback HTTP server rather than stubbing, so the streaming path is exercised for real: one offers 600 KiB with Transfer-Encoding: chunked and asserts the client gave up long before receiving it all, one asserts an oversized Content-Length is refused before reading, one asserts a normal body still round-trips. Suite green on 3.2.11 / 3.3.8 / 3.4.8.


One of a series from a security and API-coverage audit. Branches are independent, each off main.

🤖 Generated with Claude Code

Nothing bounded how much the client would read. Net::HTTP buffers a whole
response before handing it over, so a hostile endpoint, a proxy serving a
runaway error page, or a compromised upstream could hand back gigabytes
and the first code with an opinion about it was JSON.parse, by which time
the bytes were already resident. Error bodies were worse: they are kept
on the ApiError, so an oversized 500 body outlived the request.

Cap it. max_response_bytes defaults to 10 MiB, which no decision response
approaches -- the entire request is capped at 64k tokens and the answers
are smaller than the questions -- so the limit only ever bites on
something that has gone wrong.

The default transport now reads in chunks and stops at the ceiling rather
than collecting the body first, and an honest Content-Length over the
limit ends it before a single chunk arrives. A body from a custom
transport is measured on the way back, which cannot un-read it but does
keep it out of the parser and off the exception.

ResponseTooLarge is a RubyDecisionModel::Error, so the retry loop passes
it straight through: reading a too-large body again would only be slower.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant