0.15.0 — status-agnostic response-body cap
httpware 0.15.0 — status-agnostic response-body cap (max_response_body_bytes)
Minor release. Contains one breaking change (pre-1.0): the opt-in
max_error_body_bytes parameter is replaced by max_response_body_bytes.
This release turns the error-only body guard into a real, status-agnostic memory
cap that is actually enforced on the non-streaming send() path and against
compression bombs.
Breaking change
max_error_body_bytes is removed and replaced by max_response_body_bytes
on both Client and AsyncClient. There is no compatibility alias — passing the
old keyword raises TypeError.
# before
client = AsyncClient(max_error_body_bytes=1_000_000)
# after
client = AsyncClient(max_response_body_bytes=1_000_000)None (the default) remains unbounded. A non-None value below 1 is now
rejected with ValueError at construction.
What changed and why
The old max_error_body_bytes only fired inside stream(), only on 4xx/5xx, and
only as a declared-Content-Length pre-check. For a non-streaming send(),
httpx2 buffered the whole body before httpware got control, so the hot path had
no cap at all — and a small compressed body could decode to something enormous
(a 133-byte gzip body decodes to 100 KB; real bombs run ~1000:1) and slip past a
header check entirely.
max_response_body_bytes:
- Is status-agnostic — a
200is capped the same as a500. Memory
exhaustion has no status code, and the success path is the larger surface. - Counts decoded bytes (the in-memory footprint), so compression bombs are
caught. - Is enforced at the non-streaming terminal (
send()and the per-verb helpers)
via a streaming capped-accumulator, and onstream()'s internal error
pre-read. User-drivenstream()iteration is never capped. - Fails fast with
ResponseTooLargeError, which now carries areasonfield:
"declared"(declaredContent-Lengthover the cap, rejected before a byte is
read) or"streamed"(the decoded body crossed the cap mid-read).
The declared Content-Length is kept only as an early reject (never an early
accept), so chunked and bomb bodies are always run through the accumulator.
Semantics
ResponseTooLargeErroris a non-statusClientError: it is not retried and
does not count toward the circuit breaker.- An otherwise-retryable 5xx whose body exceeds the cap surfaces as
ResponseTooLargeError(cap-wins / fail-hard), not the status error — retrying
would only re-fetch the oversized body. - On the capped path the buffered response is rebuilt via the public
httpx2.Response(content=...)constructor and therefore has no.elapsed. The
default (None-cap) fast path keeps plainsend()and preserves.elapsed.
All public API is honored — no httpx2 private access.