Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion architecture/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
2 changes: 1 addition & 1 deletion architecture/testing.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 21 additions & 12 deletions planning/audits/2026-07-13-docs-comments-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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.
Original file line number Diff line number Diff line change
@@ -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.