diff --git a/architecture/errors.md b/architecture/errors.md index 73b325d..2522dc2 100644 --- a/architecture/errors.md +++ b/architecture/errors.md @@ -20,7 +20,7 @@ The "no `__init__` override" rule scopes only to `StatusError` subclasses. Non-s 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. -`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. +`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`. ## Security: request headers are reachable via `exc.response.request` diff --git a/architecture/testing.md b/architecture/testing.md index f5daed6..3a512f7 100644 --- a/architecture/testing.md +++ b/architecture/testing.md @@ -1,7 +1,7 @@ # Testing - **`pytest-asyncio` auto mode.** Async test functions do not require `@pytest.mark.asyncio`. The setting lives in `pyproject.toml` under `[tool.pytest.ini_options]`. -- **`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. +- **`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. - **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. - **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`. - **Coverage is 100% line coverage.** New code is expected to maintain this. diff --git a/docs/testing.md b/docs/testing.md index e922238..57a8067 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -107,7 +107,7 @@ For middleware with state-keeping (counters, circuit-breaker state), assert on i ## Why not `respx`? -`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. +`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. ## See also diff --git a/planning/audits/2026-07-13-docs-comments-audit.md b/planning/audits/2026-07-13-docs-comments-audit.md index 0760510..a4ac1d9 100644 --- a/planning/audits/2026-07-13-docs-comments-audit.md +++ b/planning/audits/2026-07-13-docs-comments-audit.md @@ -24,13 +24,15 @@ findings. **Verdict on your compaction/dedup question:** worth doing, but narrowly — the `src/` comment surface is already lean (the first-pass sweep found zero comments that just restate obvious code; every comment/docstring earns its -keep). The dedup opportunity is entirely on the docs side, and it's -concentrated: `ResponseTooLargeError`'s behavior is spelled out near-verbatim -in **three** places (D1) and the "why not respx" paragraph in two (D2, tangled -up with the `httpx`/`httpx2` slip in I4) — both good candidates for "full -account in one place, cross-reference from the rest," the same pattern -`architecture/` already uses elsewhere. The `CircuitBreaker` overlap (D3) is a -lower-priority, mostly-intentional tutorial-vs-reference depth split. +keep). The dedup opportunity was entirely on the docs side, and concentrated: +`ResponseTooLargeError`'s behavior was spelled out near-verbatim in **three** +places (D1, resolved `2026-07-13.09`) and the "why not respx" paragraph in two +(D2, resolved `2026-07-13.09`) — both fixed as "full account in one place, +cross-reference from the rest," the same pattern `architecture/` already uses +elsewhere. Working D2 also surfaced and fixed a factual regression in I4's +original mechanical fix (see I4's correction note). The `CircuitBreaker` +overlap (D3) remains a lower-priority, mostly-intentional +tutorial-vs-reference depth split, deferred. ## Findings @@ -76,16 +78,18 @@ This is a fresh drift, not a re-flag of the 2026-06-13 audit's I1 (that finding `"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."` 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. *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"). -**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. +**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. ### Duplication / compaction candidates **D1 — `ResponseTooLargeError` behavior is spelled out near-verbatim in three files.** `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. *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. +**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. **D2 — "why not respx" is duplicated between `docs/testing.md:108-110` and `architecture/testing.md:4`.** 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. +**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. **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. @@ -103,10 +107,15 @@ Same argument, ~3 near-identical sentences in each. Bundle this cleanup with the - **`2026-07-13.08-contributing-docstring-ci-wording`** (lightweight) — fixes I2, I3 per maintainer ruling (unconditional docstrings; link instead of restate). Verified: `mkdocs build --strict`, `just lint-ci` clean. +- **`2026-07-13.09-response-too-large-respx-compaction`** (lightweight) — + fixes D1, D2, plus a correction to I4's `2026-07-13.07` fix (the + `httpx`/`httpx2` breakage claim). Verified against `respx`'s own README and + GitHub history before writing the replacement text; `mkdocs build + --strict`, `just lint-ci`, `just test` (780 passed, 100% coverage) all + clean. ## Deferred / next steps -- **Compaction:** D1 (`ResponseTooLargeError` triplication) and D2 (bundled - with I4's `httpx`/`httpx2` fix — the "why not respx" duplication itself - wasn't touched by `2026-07-13.07`, only the terminology slip within it) are - still open. D3 is a defer-until-touched item, not worth its own change. +- D3 (`CircuitBreaker` docs/resilience.md ↔ architecture/resilience.md + overlap) is a defer-until-touched item, not worth its own change — see the + Duplication section above. diff --git a/planning/changes/2026-07-13.09-response-too-large-respx-compaction.md b/planning/changes/2026-07-13.09-response-too-large-respx-compaction.md new file mode 100644 index 0000000..a211cf7 --- /dev/null +++ b/planning/changes/2026-07-13.09-response-too-large-respx-compaction.md @@ -0,0 +1,60 @@ +--- +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. +--- + +# Change: Compact ResponseTooLargeError + respx duplication (D1, D2) + +**Lane:** lightweight — 3 files, docs-only, no code, no public-API change. + +Spec: [`planning/audits/2026-07-13-docs-comments-audit.md`](../../audits/2026-07-13-docs-comments-audit.md) +(findings D1, D2). + +## Goal + +Resolve the two duplication findings deferred from the docs-and-comments +audit, and — surfaced while working D2 — correct a factual regression from +`2026-07-13.07`'s mechanical `httpx`→`httpx2` fix. + +## Approach + +**D1 — `ResponseTooLargeError` triplication** (`docs/errors.md`, +`architecture/errors.md`, `architecture/client.md` all restated the same +facts). `docs/errors.md` keeps the full field-by-field account; +`architecture/client.md` keeps the full mechanism account (enforcement +sites, `_read_capped`, construction validation — unique content, untouched). +`architecture/errors.md`'s paragraph is trimmed to only the errors-tree-specific +facts (non-status `ClientError`, retry/circuit-breaker exemption) plus a +cross-reference to the other two for the rest. + +**D2 — "why not respx" duplication, plus a factual correction.** +`2026-07-13.07` mechanically changed "httpx" → "httpx2" in the claim +`"respx patches private internals and has historically broken across httpx +major versions"`. Researched against `respx`'s own README (requires `httpx +0.25+`, no stated `httpx2` support) and its GitHub history (documented +breakage on `httpx` major bumps, e.g. the 0.28.0 compatibility PR): the +breakage claim is accurate about the original `httpx` package, not `httpx2` — +a distinct package `respx` doesn't claim to support at all. `httpx2`'s own +docs mention `respx` as a mocking option, but that reads as inherited copy +from `httpx2`'s stewardship transfer, not a verified compatibility claim. + +`docs/testing.md` keeps the full, now-corrected rationale (`MockTransport` is +first-party `httpx2`; `respx` targets `httpx`, not `httpx2`, with no stated +support and a documented history of breaking on `httpx` major-version bumps). +`architecture/testing.md` compresses to a one-line cross-reference. + +## Files + +- `architecture/errors.md` — D1: `ResponseTooLargeError` paragraph trimmed to + a cross-reference. +- `docs/testing.md` — D2: "why not respx" rewritten with the corrected, + verified rationale. +- `architecture/testing.md` — D2: same bullet compressed to a cross-reference. + +## Verification + +- [x] `mkdocs build --strict` succeeds (relative + cross-tree links resolve). +- [x] `just lint-ci` — clean. +- [x] `just test` — 780 passed, 100% coverage (docs-only change, sanity check). +- [x] Final read-through — no residual "breaks across httpx2 major versions" + claim remains; `respx`'s actual `httpx`-only scope verified against its + README and GitHub history before writing the replacement text.