Skip to content

0.15.0 — status-agnostic response-body cap

Choose a tag to compare

@lesnik512 lesnik512 released this 23 Jun 18:52
8dcf227

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 200 is capped the same as a 500. 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 on stream()'s internal error
    pre-read. User-driven stream() iteration is never capped.
  • Fails fast with ResponseTooLargeError, which now carries a reason field:
    "declared" (declared Content-Length over 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

  • ResponseTooLargeError is a non-status ClientError: 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 plain send() and preserves .elapsed.

All public API is honored — no httpx2 private access.