|
| 1 | +# httpware documentation audit — 2026-06-13 |
| 2 | + |
| 3 | +**Status:** complete |
| 4 | +**Scope:** the user-facing documentation surface — `README.md`, the published |
| 5 | +mkdocs site (`docs/index.md`, `docs/errors.md`, `docs/middleware.md`, |
| 6 | +`docs/resilience.md`, `docs/testing.md`, `docs/recipes/*`, `docs/dev/contributing.md`), |
| 7 | +and `mkdocs.yml`. The internal `architecture/` and `planning/` trees were used |
| 8 | +as the source-of-truth cross-reference, not themselves audited. |
| 9 | +**Method:** four parallel auditors over distinct slices (README+index accuracy, |
| 10 | +capability-page accuracy, recipe accuracy, and a cross-corpus readability/UX/navigation |
| 11 | +pass). Every concrete code/API claim was verified against the real source in |
| 12 | +`src/httpware/`; the modern-di recipe claim was verified against the official |
| 13 | +modern-di 2.x migration docs via Context7. The five headline accuracy findings |
| 14 | +were reproduced directly against the code before this report was written. |
| 15 | + |
| 16 | +## Summary |
| 17 | + |
| 18 | +- Bugs (false claim / broken copy-paste example): **2** |
| 19 | +- Inconsistencies (contradicts code or another doc): **3** |
| 20 | +- Readability: **3** |
| 21 | +- Onboarding / UX gaps: **6** |
| 22 | + |
| 23 | +**Verdict.** For a developer who already knows what they want, these docs are |
| 24 | +strong: the reference pages (`errors.md`, `resilience.md`) are thorough and — |
| 25 | +`errors.md` and `testing.md` entirely — accurate down to byte-for-byte hint |
| 26 | +strings and payload-field lists; example code is unusually well-commented; |
| 27 | +terminology discipline is excellent (always `httpx2`, `AsyncClient`/`Client`, |
| 28 | +consistent capability names). The weaknesses cluster in two places: **a |
| 29 | +newcomer's first ten minutes** (no "why httpware", a non-resolving first |
| 30 | +example, no base-client migration path) and **a maintenance surface that will |
| 31 | +silently drift** (README ↔ index.md duplicate ~70% of their prose, including the |
| 32 | +full observability contract). Of ~30 checkable claims on the landing pages, |
| 33 | +accuracy was near-perfect; the real bugs live in deeper reference prose and the |
| 34 | +dev/recipe docs. |
| 35 | + |
| 36 | +## Findings |
| 37 | + |
| 38 | +### Bugs |
| 39 | + |
| 40 | +**B1 — `docs/resilience.md:70` — RetryBudget token-bucket formula is wrong (rounding).** |
| 41 | +The doc gives `ceiling = int(len(deposits_in_window) * percent_can_retry) + int(min_retries_per_sec * ttl)`. |
| 42 | +The code (`src/httpware/middleware/resilience/budget.py:68`) is |
| 43 | +`math.ceil(len(self._deposits) * self._percent_can_retry) + floor`, where |
| 44 | +`floor = int(self._min_retries_per_sec * self._ttl)`. The percent term is |
| 45 | +`math.ceil`, not `int` — a material difference, not a rounding nitpick. With the |
| 46 | +default `percent_can_retry=0.2`: 3 deposits → doc says +0, code yields +1; |
| 47 | +11 deposits → doc says +2, code yields +3. Anyone sizing a budget against the |
| 48 | +documented formula under-counts permitted retries. |
| 49 | +*Fix:* change the percent term to `ceil(...)`; the floor term stays `int(...)`. |
| 50 | + |
| 51 | +**B2 — `docs/recipes/modern-di.md:24,33,99–101` — recipe is broken against current modern-di (2.x).** |
| 52 | +The recipe uses `await container.resolve(...)` and `async with Container(scope=…, groups=[…]) as container:`. |
| 53 | +Both are modern-di **1.x** shapes. Verified against the modern-di 2.x migration |
| 54 | +docs (Context7, `/modern-python/modern-di`): resolution in 2.x is **sync-only** |
| 55 | +— "the `await` keywords used for asynchronous resolution in 1.x are removed … |
| 56 | +use `container.resolve(SomeType)` directly." The root `Container` is constructed |
| 57 | +plainly and torn down with `await container.close_async()`; `async with` is shown |
| 58 | +only for `build_child_container(...)`, never the root container. The recipe links |
| 59 | +to the live (2.x) docs, so a user copy-pasting today hits `TypeError` on the |
| 60 | +`await`. The `inspect.iscoroutinefunction` claim at line 33 is an unverifiable |
| 61 | +internal-mechanism detail and should be softened to the observable behaviour. |
| 62 | +*Fix:* drop `await` on every `resolve(...)`; construct the root container plainly |
| 63 | +and close it in a `try/finally` via `await container.close_async()`; soften the |
| 64 | +finalizer-detection sentence. |
| 65 | + |
| 66 | +### Inconsistencies |
| 67 | + |
| 68 | +**I1 — `docs/dev/contributing.md:32` — "enforced by CI grep gates" is false.** |
| 69 | +No `grep` runs in any `.github/workflows/` file. CI runs only `just install |
| 70 | +lint-ci` (ruff + ty) and `just test`. The `httpx2._` grep described in |
| 71 | +`CLAUDE.md`/`architecture/overview.md` as CI-enforced is **not wired into any |
| 72 | +workflow** — it is enforced, if at all, by review. (Separately: `CLAUDE.md` and |
| 73 | +`architecture/overview.md` both assert these invariants are "CI-enforced / CI |
| 74 | +rejects PRs"; that is true for `print()` via ruff `T20` but overstated for the |
| 75 | +`httpx2._` and `from __future__` rules. Triage item, not fixed here — see below.) |
| 76 | +*Fix:* replace "CI grep gates" with the accurate enforcement (CI lint pass: |
| 77 | +ruff + ty; review for the rest). |
| 78 | + |
| 79 | +**I2 — `docs/dev/contributing.md:11` — `just lint` comment omits `eof-fixer`.** |
| 80 | +The inline comment reads `# ruff format + ruff check + ty check`; the real target |
| 81 | +(`justfile:7–11`) runs four steps — `eof-fixer .`, `ruff format`, `ruff check |
| 82 | +--fix`, `ty check`. `CLAUDE.md` lists all four correctly. |
| 83 | +*Fix:* add `eof-fixer` to the comment. |
| 84 | + |
| 85 | +**I3 — `docs/middleware.md:104` — stale "stable contracts" list (two of four).** |
| 86 | +Names only `httpware.retry` and `httpware.bulkhead` as stable observability |
| 87 | +contracts. Since the 0.10.0 circuit-breaker/timeout work, `httpware.circuit_breaker` |
| 88 | +and `httpware.timeout` are equally stable — `index.md:153–159` lists all four and |
| 89 | +`architecture/resilience.md:23` states their event names "join `retry.*` / |
| 90 | +`bulkhead.*` as the stable observability surface; renames are breaking changes." |
| 91 | +*Fix:* list all four. |
| 92 | + |
| 93 | +(Also reclassified during audit: **`docs/resilience.md:236`** — the `AsyncTimeout` |
| 94 | +parameter table says only "`≤0` raises `ValueError`", but the code |
| 95 | +(`timeout.py:47`) and `architecture/resilience.md:17` also reject non-finite |
| 96 | +values (`inf`/`nan`). Narrower than both code and the truth home. Fixed with the |
| 97 | +batch.) |
| 98 | + |
| 99 | +### Readability |
| 100 | + |
| 101 | +**R1 — dense stacked-qualifier sentences in the hottest spots.** `README.md:31` |
| 102 | +(decoder resolution) stacks two qualified clauses; the `respect_retry_after` |
| 103 | +cell in `docs/resilience.md:24` is a four-sentence paragraph inside a table cell. |
| 104 | +Correct, but hard on first read. *Suggest:* split the densest sentences/cells. |
| 105 | + |
| 106 | +**R2 — unglossed jargon on first use.** "Finagle-style" (`README.md:17`), |
| 107 | +"full-jitter", "bulkhead", "PEP 678 note", "token bucket" — most are defined |
| 108 | +later or never, but the README is first contact. *Suggest:* a one-clause gloss on |
| 109 | +first use. |
| 110 | + |
| 111 | +**R3 — `_LOGGER` used in `errors.md` examples (lines 77, 130, 154) without |
| 112 | +definition.** A literal copy-paste hits `NameError`. Minor/conventional. |
| 113 | + |
| 114 | +### Onboarding & UX gaps (the larger lane — not bugs) |
| 115 | + |
| 116 | +These are the high-leverage adoption improvements; they require design, not |
| 117 | +mechanical edits, and are recorded here for triage rather than fixed in the |
| 118 | +accompanying change. |
| 119 | + |
| 120 | +- **G1 — No "why httpware".** Both README and index lead with "thin wrapper over |
| 121 | + httpx2" + a feature list. The actual selling points (typed errors without |
| 122 | + `raise_for_status()`; typed bodies via `response_model=`) are buried mid-page. |
| 123 | + *Suggest:* a 3-bullet "Why httpware" block up top. |
| 124 | +- **G2 — No base-client migration guide.** httpware explicitly supersedes |
| 125 | + community-of-python/base-client, yet that inbound audience has zero signpost in |
| 126 | + the user docs. *Suggest:* `docs/migration-from-base-client.md`. |
| 127 | +- **G3 — README ↔ index.md ~70% duplicated**, including the entire observability |
| 128 | + contract table — guaranteed to drift on the next logger/event change. *Suggest:* |
| 129 | + one canonical home (docs/); README becomes value-prop + install + one quickstart |
| 130 | + + links. |
| 131 | +- **G4 — First quickstart hits `https://example.test`**, which resolves to |
| 132 | + nothing — a newcomer's first paste yields `NetworkError`, not data. *Suggest:* a |
| 133 | + real public test endpoint for the leading example. |
| 134 | +- **G5 — `STATUS_TO_EXCEPTION` is a public `__all__` export |
| 135 | + (`src/httpware/__init__.py:54`) documented nowhere.** The lone undocumented |
| 136 | + public symbol. *Suggest:* document it (it is the extensible status→exception |
| 137 | + map) or reconsider its place in `__all__`. |
| 138 | +- **G6 — No custom-`ResponseDecoder` guide and no API reference.** The decoder |
| 139 | + seam (Seam B) is a documented extension point but, unlike middleware, gets no |
| 140 | + "write your own" guide; and there is no generated symbol reference |
| 141 | + (mkdocstrings) for constructor/method signatures. *Suggest:* a short Decoders |
| 142 | + guide + minimal mkdocstrings page. |
| 143 | + |
| 144 | +Navigation nits (LOW): mkdocs nav orders Resilience before Middleware though |
| 145 | +Resilience is built on it and forward-references it; several `architecture/*.md` |
| 146 | +references in published pages are bare paths, not links, so a site reader cannot |
| 147 | +follow them. No orphan pages and no broken nav targets — the nav is otherwise clean. |
| 148 | + |
| 149 | +### Verified correct (negative results) |
| 150 | + |
| 151 | +- `README.md` + `docs/index.md`: every import resolves against `__all__`; all |
| 152 | + constructor signatures/params (`base_url`, `decoders`, `response_model`, |
| 153 | + `httpx2_client`, `middleware`, `max_concurrent`), the exception tree, the extras |
| 154 | + (`httpware[pydantic|msgspec|otel|all]` vs `pyproject.toml`), and the behavioural |
| 155 | + claims (auto-raise on 4xx/5xx, `MissingDecoderError` before the HTTP call, |
| 156 | + pydantic-first default, stream bypasses middleware + pre-reads body on error, |
| 157 | + `_emit_event` OTel call) are literally true. Zero bugs. |
| 158 | +- `docs/errors.md`: exception tree, `STATUS_TO_EXCEPTION` mapping table, the |
| 159 | + 400≤status<600 fallback, the StatusError-single-positional / ClientError-keyword |
| 160 | + `__init__` split, the doubly-inherited `TimeoutError`, every resilience-error |
| 161 | + payload field, and both `MissingDecoderError` hint strings match `errors.py` |
| 162 | + byte-for-byte. |
| 163 | +- `docs/resilience.md`: every constructor default verified against source |
| 164 | + (`max_attempts=3`, `base_delay=0.1`, `max_delay=5.0`, |
| 165 | + `retry_status_codes={408,429,502,503,504}`, `failure_threshold=5`, |
| 166 | + `reset_timeout=30.0`, `success_threshold=1`, budget `ttl=10.0`, |
| 167 | + `min_retries_per_sec=10.0`, `percent_can_retry=0.2`, bulkhead `acquire_timeout=1.0`, |
| 168 | + …); 429-as-success, OPEN/HALF_OPEN/CLOSED behaviour, Retry-After parsing/clamping, |
| 169 | + and composition ordering all correct. |
| 170 | +- `docs/middleware.md`: protocol definitions, `Next`/`AsyncNext` aliases, the three |
| 171 | + phase-decorator signatures (incl. `on_error` catching `Exception` not |
| 172 | + `BaseException`), and the compose-once-and-frozen claim all match. |
| 173 | +- `docs/testing.md`: the `httpx2.MockTransport`-via-`httpx2_client=` pattern matches |
| 174 | + the real tests and `architecture/testing.md`; the "`RecordedTransport` removed" |
| 175 | + reference is correct. |
| 176 | +- `docs/recipes/link-header-pagination.md` and `phase-decorator-patterns.md`: every |
| 177 | + API call matches source; both are copy-paste-safe. |
| 178 | + |
| 179 | +## Spawned changes |
| 180 | + |
| 181 | +- **`2026-06-13.04-docs-accuracy-fixes`** (lightweight) — fixes B1, B2, I1, I2, I3, |
| 182 | + and the `AsyncTimeout`-validation wording. All verified against code / official |
| 183 | + upstream docs. |
| 184 | + |
| 185 | +## Deferred / triage |
| 186 | + |
| 187 | +- The onboarding & UX gaps (G1–G6) — a separate, larger docs-UX change (de-dup, |
| 188 | + why-httpware, base-client migration, runnable quickstart, custom-decoder guide, |
| 189 | + API reference). Not yet scheduled. |
| 190 | +- The `httpx2._` invariant is documented as CI-enforced (`CLAUDE.md`, |
| 191 | + `architecture/overview.md`, and — fixed here — `contributing.md`) but no CI |
| 192 | + workflow runs the grep. Either wire `grep -rE 'httpx2\._' src/httpware/` into the |
| 193 | + lint workflow (tiny CI change) or downgrade the "CI-enforced" wording in the two |
| 194 | + internal truth docs. Parked pending a decision on which. |
0 commit comments