Skip to content

feat(story-1.3): exception hierarchy with plain typed fields#3

Merged
lesnik512 merged 1 commit into
mainfrom
story/1-3-exception-hierarchy-with-plain-fields
May 13, 2026
Merged

feat(story-1.3): exception hierarchy with plain typed fields#3
lesnik512 merged 1 commit into
mainfrom
story/1-3-exception-hierarchy-with-plain-fields

Conversation

@lesnik512
Copy link
Copy Markdown
Member

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.

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>
@lesnik512 lesnik512 self-assigned this May 13, 2026
@lesnik512 lesnik512 merged commit 2e49b61 into main May 13, 2026
9 of 10 checks passed
@lesnik512 lesnik512 deleted the story/1-3-exception-hierarchy-with-plain-fields branch May 13, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant