Skip to content

feat(errors): typed 4xx subclasses + validation envelope handler (closes #32)#47

Merged
thinmintdev merged 2 commits into
mainfrom
fix/issue-32-typed-4xx
May 16, 2026
Merged

feat(errors): typed 4xx subclasses + validation envelope handler (closes #32)#47
thinmintdev merged 2 commits into
mainfrom
fix/issue-32-typed-4xx

Conversation

@thinmintdev
Copy link
Copy Markdown
Contributor

Summary

Closes #32.

  • Adds typed Hal0Error 4xx subclasses (BadRequest 400, Unauthorized 401, Forbidden 403, NotFound 404, Conflict 409, UnprocessableEntity 422) in hal0.errors, each with a sensible default code and an optional per-instance code= override so routes can pick a stable namespaced code per error site without sub-subclassing.
  • Registers a RequestValidationError handler in hal0.api.middleware.error_codes that reshapes FastAPI's default {"detail": [...]} 422 into the canonical envelope {"error": {"code, message, details: {errors: [...]}}} with code="validation.invalid", downgrading the status to 400 (caller-side input errors, not "well-formed but semantically rejected" — that meaning is reserved for explicit UnprocessableEntity raises).
  • Re-exports the new subclasses from hal0.api.middleware.error_codes so routes can use the import path most of them already use.

Scope

This is the foundation for the wave-2 cleanup (#34/#35/#36/#37) — those issues should rebase on this branch. The PR only refactors two sanity-test call sites (auth + slots body validation) to prove the round-trip; bulk migration of every existing Hal0Error("...") raise is intentionally out of scope here.

The /v1/* (OpenAI-shaped) error path is untouched — those routes already use typed DispatchError subclasses and share the same middleware envelope today.

What's new

  • hal0.errors.BadRequest
  • hal0.errors.Unauthorized
  • hal0.errors.Forbidden
  • hal0.errors.NotFound
  • hal0.errors.Conflict
  • hal0.errors.UnprocessableEntity

All re-exported from hal0.api.middleware.error_codes.

Handler registration (lives inside install(app) in error_codes.py):

@app.exception_handler(RequestValidationError)
async def _validation_handler(request, exc): ...

Test plan

  • tests/api/test_typed_errors.py parametrizes all six subclasses against status + envelope round-trip, plus code= override, details defaulting, and the legacy sub-subclass pattern.
  • Three pydantic-driven validation scenarios covered: missing query param, missing body field, invalid enum value.
  • Manually-raised UnprocessableEntity keeps its 422 (distinct from the auto-downgraded pydantic path).
  • tests/api/test_logs_routes.py updated for the new envelope shape on missing/out-of-range params.
  • Full test suite: 665 passed, 6 skipped (pytest tests/ --ignore=tests/harness).
  • ruff check + ruff format --check clean on touched files.
  • mypy clean on hal0/errors.py + hal0/api/middleware/error_codes.py.

🤖 Generated with Claude Code

 #32)

Add ergonomic ``Hal0Error`` subclasses for the everyday HTTP client-error
statuses — ``BadRequest`` (400), ``Unauthorized`` (401), ``Forbidden``
(403), ``NotFound`` (404), ``Conflict`` (409), ``UnprocessableEntity``
(422). Each carries a sensible default ``code`` and accepts a per-instance
``code=`` override so route sites get the right status + envelope without
sub-subclassing for every distinct error.

Register a ``RequestValidationError`` exception handler that reshapes
FastAPI's default ``{"detail": [...]}`` 422 into the canonical hal0
envelope ``{"error": {"code, message, details}}`` with
``code="validation.invalid"``, downgrading the status to 400 (caller-side
input errors, not "well-formed but semantically rejected").

Touches two sanity-test call sites only (auth + slots body validation);
the wave-2 migration of every existing bare ``Hal0Error("...")`` raise
is intentionally out of scope (issues #34/#35/#36/#37 will rebase on
this).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thinmintdev added a commit that referenced this pull request May 16, 2026
Format-only — unblocks CI for #45/#46/#47/#48/#49/#50.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinmintdev thinmintdev merged commit d61f1ce into main May 16, 2026
0 of 6 checks passed
@thinmintdev thinmintdev deleted the fix/issue-32-typed-4xx branch May 21, 2026 20:11
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.

Typed 4xx subclasses + FastAPI validation envelope handler

1 participant