Skip to content

Commit b147e2c

Browse files
lesnik512claude
andcommitted
feat(story-1.3): exception hierarchy with plain typed fields
Status-keyed exception hierarchy with plain typed fields under `src/httpware/errors.py`: `ClientError` root; `TransportError` and `TimeoutError` direct subclasses; `StatusError` with kwargs-only 6-field `__init__` and a redacting `__repr__`; `ClientStatusError` and `ServerStatusError` category bases; 9 status-leaf classes (`BadRequestError` 400 … `ServiceUnavailableError` 503); `STATUS_TO_EXCEPTION` lookup dict. Adversarial code review (Blind + Edge Case + Acceptance Auditor) flagged seven decisions; six landed as in-scope patches, one accepted as v0: - `StatusError.__reduce__` makes exceptions picklable / deep-copyable across process boundaries (multiprocessing, pytest-xdist, Sentry). - `__repr__` and the `Exception.__init__` summary message strip `user:pass@` userinfo from the request URL to close the NFR8 leak path; query-string redaction stays with Story 5.3's `Redactor`. - `TimeoutError` multi-inherits `(ClientError, builtins.TimeoutError)` so `except builtins.TimeoutError` (asyncio.wait_for's form) also catches httpware-raised timeouts. Revisits architecture Decision 3. - Module docstring + `STATUS_TO_EXCEPTION` comment scope the fallback rule to `400 <= status < 600`; callers must guard non-error statuses (1xx/2xx/3xx) before consulting the dict. - `headers` is stored as a read-only `MappingProxyType(dict(headers))` so caller mutations after `raise` cannot bleed into the exception. - Story Dev Notes canonical `__all__` updated to match the RUF022 order ruff enforces in CI. 43 tests in `tests/test_errors.py` (29 from the original story spec + 14 from the review patches); 70/70 project-wide pass; coverage 100% on `errors.py` and every other module. `just lint` clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 805fcd0 commit b147e2c

6 files changed

Lines changed: 983 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1616
- `CONTRIBUTING.md` with development workflow.
1717
- `CLAUDE.md` with AI-agent guidance.
1818
- Core data types: `Request`, `Response`, `Limits`, `Timeout`, `ClientConfig` — frozen+slotted dataclasses with `with_*` immutability helpers on `Request` and computed `text`/`json()` accessors on `Response` (Story 1.2).
19+
- Status-keyed exception hierarchy with plain typed fields: `ClientError`, `TransportError`, `TimeoutError`, `StatusError`, `ClientStatusError`/`ServerStatusError` bases, 9 leaf classes (`BadRequestError``ServiceUnavailableError`), `STATUS_TO_EXCEPTION` lookup dict (Story 1.3). `StatusError` is picklable and deep-copyable via custom `__reduce__`; `__repr__` and the summary message strip `user:pass@` userinfo from the request URL; `headers` is stored as a read-only `MappingProxyType` so caller mutations after `raise` do not bleed into the exception. `TimeoutError` multi-inherits from `builtins.TimeoutError` (revisits architecture Decision 3) so `except builtins.TimeoutError` (the form `asyncio.wait_for` raises) also catches httpware-raised timeouts.
1920

2021
[Unreleased]: https://github.com/modern-python/httpware/commits/main

docs/deferred-work.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
Items raised in reviews that are real but not actionable now.
44

5+
## Deferred from: code review of story-1-3 (2026-05-13)
6+
7+
- **`request_method` / `request_url` CRLF / log-injection** — fields stored verbatim; `repr` echoes embedded `\r\n`. Transport seam should validate before crafting the request. (`src/httpware/errors.py:55,58`) — revisit in Story 1.4.
8+
- **Header case-folding contract**`Mapping[str, str]` doesn't pin case-sensitivity. `exc.headers["Content-Type"]` may `KeyError` if `httpx2.Headers` hands in lowercased keys. (`src/httpware/errors.py:33`) — Story 1.4 transport seam decides.
9+
- **`request_method` casing normalization**`repr` faithfully echoes `"get"` / `"Get"` if middleware lowercases. Document expected casing or normalize at the seam. (`src/httpware/errors.py:35`) — Story 1.4.
10+
511
## Deferred from: code review of story-1-2 (2026-05-13)
612

713
- **Charset parser robustness** — quoted whitespace, mismatched quotes, multi-`charset=` directives, substring false-positives (e.g. `boundary` containing `charset=`). (`src/httpware/response.py:21-26`)

0 commit comments

Comments
 (0)