feat!: status-agnostic response-body cap (max_response_body_bytes)#78
Merged
Conversation
Status-agnostic semantics + explicit declared/streamed trip mode, ahead of the max_response_body_bytes cap rework. Existing stream() Content-Length checks pass reason="declared"; restructured in a later task. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_read_capped / _read_capped_async wrap the pure core with the Content-Length early-reject and the buffered Response rebuild; _safe_extensions drops the stale network_stream. Caller owns the stream lifecycle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Status-agnostic, decoded-byte cap enforced at the non-streaming terminal via a streaming capped-accumulator (send(stream=True) + _read_capped). Branches on cap is None so the default path keeps plain send() and .elapsed. Construction validates >= 1. The old error-only param is removed (pre-1.0, no shim). BREAKING CHANGE: max_error_body_bytes is replaced by max_response_body_bytes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both stream() error branches route the 4xx/5xx pre-read through the shared accumulator when a cap is set, so chunked/compression-bombed error bodies are caught instead of read unbounded; exc.response.content stays populated. User-driven success streaming is never capped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cap trips are not retried, do not trip the circuit breaker, and an over-cap retryable 5xx surfaces as ResponseTooLargeError (cap-wins). No prod change — these assert the behavior that falls out of the ClientError hierarchy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite architecture/client.md 'Bounded response bodies' and the errors.md ResponseTooLargeError entry for the status-agnostic decoded-byte cap; remove the actioned deferred item; add the 0.15.0 release note; check in the change bundle (design.md + plan.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bugs in the capped read path found in review of #78: - The rebuilt Response kept content-encoding while holding already-decoded content, so httpx2 re-decompressed and crashed on every compressed body under the cap. Strip content-encoding / transfer-encoding / (compressed) content-length via _buffered_headers; httpx2 recomputes content-length. - A bodiless response (HEAD / 204 / 304) with a large declared Content-Length was falsely rejected. _response_has_body short-circuits these: read the empty body and return the original response unchanged, preserving its headers (HEAD legitimately echoes the entity length). Adds within-cap compressed-body and bodiless regression tests at the _read_capped, send(), and stream() levels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the error-only
max_error_body_bytesknob with a status-agnostic, decoded-bytemax_response_body_bytescap that is actually enforced on the non-streamingsend()path and against compression bombs. Entirely public httpx2 API — nohttpx2._. Off by default (None).Closes the 2026-06-14 deep-audit "Non-streaming hard response-body cap" deferred item.
max_error_body_bytesis removed with no alias; passing it raisesTypeError. Migrate tomax_response_body_bytes.Why
max_error_body_bytesonly fired insidestream(), only on 4xx/5xx, and only as a declared-Content-Lengthpre-check. A non-streamingsend()buffered the whole body before httpware got control (no cap at all on the hot path), and a 133-byte gzip body decoding to 100 KB (real bombs ~1000:1) slipped straight past a header check.What changed
200is capped the same as a500; the success path is the larger memory-exhaustion surface.Content-Lengthis kept as an early reject only, never an early accept._read_cappedaccumulator at the non-streaming terminal (send(request, stream=True)+ accumulate) and onstream()'s internal error pre-read. User-drivenstream()iteration is never capped.None-cap) keeps the plainsend()fast path — zero streaming overhead,.elapsedpreserved.ResponseTooLargeErrorgains areasondiscriminator:"declared"vs"streamed".>= 1(ValueError).Semantics
ResponseTooLargeErroris a non-statusClientError: not retried, does not count toward the circuit breaker.ResponseTooLargeError(cap-wins / fail-hard), not the status error.httpx2.Response(content=...), which has no.elapsed(documented caveat);None-cap path preserves it.Tests
756 passing, 100% coverage. Adds: Hypothesis property test for chunk-boundary independence of the pure
_accumulate_cappedcore; sync+async unit tests for_read_capped(declared / streamed / gzip-bomb / boundary / empty); terminalsend()cap + validation; boundedstream()error pre-read + uncapped user streaming; resilience-interaction locks (no-retry / no-breaker-trip / cap-wins).Docs
architecture/client.md(Bounded response bodies) andarchitecture/errors.md(ResponseTooLargeError) rewritten; deferred item retired;planning/releases/0.15.0.mdadded; design + plan bundle underplanning/changes/2026-06-23.03-response-body-cap/.🤖 Generated with Claude Code