Skip to content

Commit 9ac42ad

Browse files
committed
docs: dedupe ResponseTooLargeError + respx rationale, fix httpx2 claim
ResponseTooLargeError's behavior was spelled out near-verbatim in docs/errors.md, architecture/errors.md, and architecture/client.md; architecture/errors.md now cross-references the other two instead of restating them. The "why not respx" paragraph was duplicated between docs/testing.md and architecture/testing.md, and the prior audit-fix pass (#103) had mechanically changed a "breaks across httpx major versions" claim to say httpx2 instead. Verified against respx's own README (requires httpx 0.25+, no stated httpx2 support) and GitHub history: the breakage claim is real but about httpx, not httpx2. Corrected the claim and deduped, keeping the full rationale in docs/testing.md.
1 parent c87b405 commit 9ac42ad

5 files changed

Lines changed: 84 additions & 15 deletions

File tree

architecture/errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The "no `__init__` override" rule scopes only to `StatusError` subclasses. Non-s
2020

2121
These six non-status `ClientError` subclasses inherit `__reduce__` from `_KeywordReduceMixin`, which pickles via `self.__dict__` and reconstructs via `cls(**kwargs)`. This requires `self.__dict__` to exactly mirror the `__init__` keyword parameters: an attribute stored beyond those parameters raises `TypeError` on unpickle (unexpected keyword argument), and a keyword parameter `__init__` doesn't assign to `self` is silently dropped if it has a default (unpickle reverts to it) or raises `TypeError` if it doesn't.
2222

23-
`ResponseTooLargeError` is raised when `max_response_body_bytes` is set and a response body would exceed the cap — status-agnostic (a `200` can trip it), counting **decoded** bytes. It fires from the non-streaming terminal (`send()`) and from `stream()`'s internal error pre-read; user-driven `stream()` iteration is never capped. The `reason` field discriminates the two trip modes: `"declared"` (the declared `Content-Length` already exceeds the cap, rejected before any byte is read — `content_length` holds it) and `"streamed"` (the decoded body crossed the cap mid-read, the chunked or compression-bomb case, where the true size is unknown by design). It is a non-status `ClientError`; it does not carry a `StatusError`-style positional `response` and is not in `STATUS_TO_EXCEPTION`. Because it is neither a `StatusError`, `NetworkError`, nor `TimeoutError`, it is not retried and does not count toward the circuit breaker.
23+
`ResponseTooLargeError` is a non-status `ClientError`; it does not carry a `StatusError`-style positional `response` and is not in `STATUS_TO_EXCEPTION`. Because it is neither a `StatusError`, `NetworkError`, nor `TimeoutError`, it is not retried and does not count toward the circuit breaker. The cap mechanism itself — when it fires, the two enforcement sites, and the `declared`/`streamed` `reason` split — is documented in [`client.md`](client.md); the full field-by-field API is in `docs/errors.md`.
2424

2525
## Security: request headers are reachable via `exc.response.request`
2626

architecture/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Testing
22

33
- **`pytest-asyncio` auto mode.** Async test functions do not require `@pytest.mark.asyncio`. The setting lives in `pyproject.toml` under `[tool.pytest.ini_options]`.
4-
- **`httpx2.MockTransport` for transport mocking, not `respx`.** Tests construct `httpx2.AsyncClient(transport=httpx2.MockTransport(handler))` and pass it as `httpx2_client=` to `AsyncClient` (or the sync equivalent `httpx2.Client(transport=httpx2.MockTransport(handler))` to `Client`). `MockTransport` is the public test seam in `httpx2`; `respx` patches private internals and breaks across `httpx` major versions.
4+
- **`httpx2.MockTransport` for transport mocking, not `respx`.** Tests construct `httpx2.AsyncClient(transport=httpx2.MockTransport(handler))` and pass it as `httpx2_client=` to `AsyncClient` (or the sync equivalent `httpx2.Client(transport=httpx2.MockTransport(handler))` to `Client`). `MockTransport` is a first-party `httpx2` API; `respx` targets `httpx` (not `httpx2`) and patches its internals directly — see `docs/testing.md` for why.
55
- **Hypothesis property-based tests** for concurrency-sensitive code: `RetryBudget`, `Bulkhead`, retry interleaving. Files are named `test_*_props.py` so they are easy to grep and treat separately in CI.
66
- **Performance tests are opt-in.** The `perf` pytest marker is registered in `pyproject.toml`; the default `addopts` line includes `-m 'not perf'`. Run benchmarks explicitly with `pytest -m perf`.
77
- **Coverage is 100% line coverage.** New code is expected to maintain this.

docs/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ For middleware with state-keeping (counters, circuit-breaker state), assert on i
107107

108108
## Why not `respx`?
109109

110-
`httpware` deliberately uses `httpx2.MockTransport` instead of `respx` for its own tests. `MockTransport` is the public test seam in `httpx2` — supported by the maintainers, stable across versions, lives in the public API surface. `respx` patches private internals and has historically broken across `httpx2` major versions. Stick with `MockTransport` unless you have a specific reason not to.
110+
`httpware` deliberately uses `httpx2.MockTransport` instead of `respx` for its own tests. `MockTransport` is a first-party `httpx2` API — supported by the maintainers, stable across versions, part of the public API surface. `respx` targets the original `httpx` package (its README requires `httpx 0.25+`, with no stated `httpx2` support) and has a documented history of breaking across `httpx` major-version bumps, since it patches `httpx`/`httpcore` internals directly. Stick with `MockTransport` unless you have a specific reason not to.
111111

112112
## See also
113113

planning/audits/2026-07-13-docs-comments-audit.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ findings.
2424
**Verdict on your compaction/dedup question:** worth doing, but narrowly — the
2525
`src/` comment surface is already lean (the first-pass sweep found zero
2626
comments that just restate obvious code; every comment/docstring earns its
27-
keep). The dedup opportunity is entirely on the docs side, and it's
28-
concentrated: `ResponseTooLargeError`'s behavior is spelled out near-verbatim
29-
in **three** places (D1) and the "why not respx" paragraph in two (D2, tangled
30-
up with the `httpx`/`httpx2` slip in I4) — both good candidates for "full
31-
account in one place, cross-reference from the rest," the same pattern
32-
`architecture/` already uses elsewhere. The `CircuitBreaker` overlap (D3) is a
33-
lower-priority, mostly-intentional tutorial-vs-reference depth split.
27+
keep). The dedup opportunity was entirely on the docs side, and concentrated:
28+
`ResponseTooLargeError`'s behavior was spelled out near-verbatim in **three**
29+
places (D1, resolved `2026-07-13.09`) and the "why not respx" paragraph in two
30+
(D2, resolved `2026-07-13.09`) — both fixed as "full account in one place,
31+
cross-reference from the rest," the same pattern `architecture/` already uses
32+
elsewhere. Working D2 also surfaced and fixed a factual regression in I4's
33+
original mechanical fix (see I4's correction note). The `CircuitBreaker`
34+
overlap (D3) remains a lower-priority, mostly-intentional
35+
tutorial-vs-reference depth split, deferred.
3436

3537
## Findings
3638

@@ -76,16 +78,18 @@ This is a fresh drift, not a re-flag of the 2026-06-13 audit's I1 (that finding
7678
`"MockTransport is the public test seam in httpx — supported by the maintainers, stable across versions ... respx patches private internals and has historically broken across httpx major versions."`
7779
Every other reference in the same file (line 3) and in `architecture/testing.md:4` correctly says `httpx2` — a real, separate PyPI package (`httpx2>=2.0.0,<3.0`, maintained by Pydantic Services Inc.), not an alias for the original `httpx`. This line reads as a leftover phrase from before the `httpx2` rename, and as written it inaccurately implies `respx`'s breakage history is about `httpx2` specifically.
7880
*Fix:* change both `httpx` occurrences on that line to `httpx2` (verify against `respx`'s actual `httpx2` support status if this section is touched — it may be that `respx` doesn't support `httpx2` at all, which is a stronger reason to use `MockTransport` than "it breaks across versions").
79-
**Resolved** (`2026-07-13.07`) — both occurrences corrected to `httpx2`; the underlying "does respx support httpx2 at all" question is left as-is, not part of this mechanical fix.
81+
**Resolved** (`2026-07-13.07`) — both occurrences corrected to `httpx2`; the underlying "does respx support httpx2 at all" question is left as-is, not part of this mechanical fix. **Correction** (`2026-07-13.09`) — that fix itself was a factual regression: it applied the "breaks across major versions" claim to `httpx2`, but the claim (verified against `respx`'s own README and GitHub history) is actually about the original `httpx` package, which `respx` targets and `httpx2` is not. Reverted the breakage clause to `httpx`; see D2.
8082

8183
### Duplication / compaction candidates
8284

8385
**D1 — `ResponseTooLargeError` behavior is spelled out near-verbatim in three files.**
8486
`docs/errors.md:193-204`, `architecture/errors.md:23`, and `architecture/client.md:36` all restate the same handful of facts (status-agnostic, counts decoded bytes, fires from the non-streaming terminal and `stream()`'s error pre-read but not user-driven iteration, the `"declared"`/`"streamed"` reason split, "neither StatusError, NetworkError, nor TimeoutError — not retried, doesn't count toward the circuit breaker") in matching or near-matching phrasing. `docs/errors.md` has the fullest account.
8587
*Suggest:* keep the full account in `docs/errors.md` (or `architecture/errors.md`, whichever is meant to be canonical for this fact), compress the other two to a one-line cross-reference.
88+
**Resolved** (`2026-07-13.09`) — `docs/errors.md` (fields) and `architecture/client.md` (mechanism) keep their full accounts; `architecture/errors.md` trimmed to the errors-tree-specific facts plus a cross-reference to both.
8689

8790
**D2 — "why not respx" is duplicated between `docs/testing.md:108-110` and `architecture/testing.md:4`.**
8891
Same argument, ~3 near-identical sentences in each. Bundle this cleanup with the I4 fix (same lines) — reconcile the `httpx`/`httpx2` wording and de-duplicate the reasoning in the same edit, keeping the fuller version in one file.
92+
**Resolved** (`2026-07-13.09`) — while researching which side to keep, found `2026-07-13.07`'s I4 fix had regressed the underlying claim (see I4's correction note above). Researched `respx` against its own README and GitHub history: it requires `httpx 0.25+`, states no `httpx2` support, and has a documented history of breaking on `httpx` major-version bumps (patches `httpx`/`httpcore` internals directly) — `httpx2`'s own docs mentioning `respx` reads as inherited copy from its stewardship transfer, not a verified compatibility claim. `docs/testing.md` now carries the corrected full rationale (`MockTransport` is first-party `httpx2`; `respx` targets `httpx`, not `httpx2`, with no stated support); `architecture/testing.md` compresses to a cross-reference.
8993

9094
**D3 — `CircuitBreaker` states/failure-classification/rate-mode overlap between `docs/resilience.md:158-212` and `architecture/resilience.md:17,21`.** Lower priority: this is mostly a legitimate tutorial-vs-compressed-reference depth split, not verbatim duplication, but several exact clauses ("4xx including 429 count as successes," the `window_seconds=30.0`/`minimum_calls=20` defaults) are copied rather than merely covering the same ground. Worth a light pass if `resilience.md` is next revised for another reason — not worth a dedicated change on its own.
9195

@@ -103,10 +107,15 @@ Same argument, ~3 near-identical sentences in each. Bundle this cleanup with the
103107
- **`2026-07-13.08-contributing-docstring-ci-wording`** (lightweight) — fixes
104108
I2, I3 per maintainer ruling (unconditional docstrings; link instead of
105109
restate). Verified: `mkdocs build --strict`, `just lint-ci` clean.
110+
- **`2026-07-13.09-response-too-large-respx-compaction`** (lightweight) —
111+
fixes D1, D2, plus a correction to I4's `2026-07-13.07` fix (the
112+
`httpx`/`httpx2` breakage claim). Verified against `respx`'s own README and
113+
GitHub history before writing the replacement text; `mkdocs build
114+
--strict`, `just lint-ci`, `just test` (780 passed, 100% coverage) all
115+
clean.
106116

107117
## Deferred / next steps
108118

109-
- **Compaction:** D1 (`ResponseTooLargeError` triplication) and D2 (bundled
110-
with I4's `httpx`/`httpx2` fix — the "why not respx" duplication itself
111-
wasn't touched by `2026-07-13.07`, only the terminology slip within it) are
112-
still open. D3 is a defer-until-touched item, not worth its own change.
119+
- D3 (`CircuitBreaker` docs/resilience.md ↔ architecture/resilience.md
120+
overlap) is a defer-until-touched item, not worth its own change — see the
121+
Duplication section above.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
summary: Deduped ResponseTooLargeError's triplicated behavior description down to one canonical account each, and corrected + deduped the "why not respx" rationale which had regressed to an unverifiable httpx2-specific claim.
3+
---
4+
5+
# Change: Compact ResponseTooLargeError + respx duplication (D1, D2)
6+
7+
**Lane:** lightweight — 3 files, docs-only, no code, no public-API change.
8+
9+
Spec: [`planning/audits/2026-07-13-docs-comments-audit.md`](../../audits/2026-07-13-docs-comments-audit.md)
10+
(findings D1, D2).
11+
12+
## Goal
13+
14+
Resolve the two duplication findings deferred from the docs-and-comments
15+
audit, and — surfaced while working D2 — correct a factual regression from
16+
`2026-07-13.07`'s mechanical `httpx``httpx2` fix.
17+
18+
## Approach
19+
20+
**D1 — `ResponseTooLargeError` triplication** (`docs/errors.md`,
21+
`architecture/errors.md`, `architecture/client.md` all restated the same
22+
facts). `docs/errors.md` keeps the full field-by-field account;
23+
`architecture/client.md` keeps the full mechanism account (enforcement
24+
sites, `_read_capped`, construction validation — unique content, untouched).
25+
`architecture/errors.md`'s paragraph is trimmed to only the errors-tree-specific
26+
facts (non-status `ClientError`, retry/circuit-breaker exemption) plus a
27+
cross-reference to the other two for the rest.
28+
29+
**D2 — "why not respx" duplication, plus a factual correction.**
30+
`2026-07-13.07` mechanically changed "httpx" → "httpx2" in the claim
31+
`"respx patches private internals and has historically broken across httpx
32+
major versions"`. Researched against `respx`'s own README (requires `httpx
33+
0.25+`, no stated `httpx2` support) and its GitHub history (documented
34+
breakage on `httpx` major bumps, e.g. the 0.28.0 compatibility PR): the
35+
breakage claim is accurate about the original `httpx` package, not `httpx2`
36+
a distinct package `respx` doesn't claim to support at all. `httpx2`'s own
37+
docs mention `respx` as a mocking option, but that reads as inherited copy
38+
from `httpx2`'s stewardship transfer, not a verified compatibility claim.
39+
40+
`docs/testing.md` keeps the full, now-corrected rationale (`MockTransport` is
41+
first-party `httpx2`; `respx` targets `httpx`, not `httpx2`, with no stated
42+
support and a documented history of breaking on `httpx` major-version bumps).
43+
`architecture/testing.md` compresses to a one-line cross-reference.
44+
45+
## Files
46+
47+
- `architecture/errors.md` — D1: `ResponseTooLargeError` paragraph trimmed to
48+
a cross-reference.
49+
- `docs/testing.md` — D2: "why not respx" rewritten with the corrected,
50+
verified rationale.
51+
- `architecture/testing.md` — D2: same bullet compressed to a cross-reference.
52+
53+
## Verification
54+
55+
- [x] `mkdocs build --strict` succeeds (relative + cross-tree links resolve).
56+
- [x] `just lint-ci` — clean.
57+
- [x] `just test` — 780 passed, 100% coverage (docs-only change, sanity check).
58+
- [x] Final read-through — no residual "breaks across httpx2 major versions"
59+
claim remains; `respx`'s actual `httpx`-only scope verified against its
60+
README and GitHub history before writing the replacement text.

0 commit comments

Comments
 (0)