feat(story-1.3): exception hierarchy with plain typed fields#3
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status-keyed exception hierarchy with plain typed fields under
src/httpware/errors.py:ClientErrorroot;TransportErrorandTimeoutErrordirect subclasses;StatusErrorwith kwargs-only 6-field__init__and a redacting__repr__;ClientStatusErrorandServerStatusErrorcategory bases; 9 status-leaf classes (BadRequestError400 …ServiceUnavailableError503);STATUS_TO_EXCEPTIONlookup 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 theException.__init__summary message stripuser:pass@userinfo from the request URL to close the NFR8 leak path; query-string redaction stays with Story 5.3'sRedactor.TimeoutErrormulti-inherits(ClientError, builtins.TimeoutError)soexcept builtins.TimeoutError(asyncio.wait_for's form) also catches httpware-raised timeouts. Revisits architecture Decision 3.STATUS_TO_EXCEPTIONcomment scope the fallback rule to400 <= status < 600; callers must guard non-error statuses (1xx/2xx/3xx) before consulting the dict.headersis stored as a read-onlyMappingProxyType(dict(headers))so caller mutations afterraisecannot bleed into the exception.__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% onerrors.pyand every other module.just lintclean.