Skip to content

feat: standardize API error responses on RFC 9457 problem details - #1158

Draft
knowald wants to merge 1 commit into
the-momentum:mainfrom
knowald:feat/rfc9457-error-format
Draft

feat: standardize API error responses on RFC 9457 problem details#1158
knowald wants to merge 1 commit into
the-momentum:mainfrom
knowald:feat/rfc9457-error-format

Conversation

@knowald

@knowald knowald commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Closes #21. Implements the proposal from my comment there
(#21 (comment)).

Every error the API emits today is one of three incompatible shapes, none carrying a
machine-readable code: {"detail": "<string>"} from the 105 raise HTTPException sites,
validation failures converted to a 400 whose string is built from only the first error
(app/main.py:70 + app/utils/exceptions.py:78, so the OpenAPI schema documented a 422
array that never hit the wire), and Starlette's plain-text 500 for unhandled exceptions.
Clients have nothing stable to switch on; the dashboard derives error categories from the
HTTP status alone.

All error responses are now RFC 9457 problem details served as application/problem+json:

{
  "title": "Not Found",
  "status": 404,
  "detail": "User with ID: 123 not found.",
  "code": "USER_NOT_FOUND"
}

What changed:

  • app/utils/problem.py (new): response builder, exception handlers (HTTPException,
    validation, DatetimeParseError, catch-all 500), and an OpenAPI patch that replaces the
    auto-generated HTTPValidationError schema with Problem so spec and wire agree.
  • ApiError(HTTPException) carries code; all 72 non-webhook raise sites migrated,
    44 distinct codes. Detail message texts are byte-for-byte unchanged.
  • Validation errors return 422 with "errors": [{field, message, type}] listing every
    failure instead of a 400 with the first one. This is the only deliberate status change.
  • Unhandled exceptions return JSON 500 with code: "INTERNAL_ERROR" and a sanitized
    message. Starlette re-raises after responding, so Sentry and server logs still get the
    exception.
  • Login failures keep 401 and the exact detail string, now with
    code: "INVALID_CREDENTIALS" and the same WWW-Authenticate header.
  • Frontend: ApiError.fromResponse parses problem json (backend code on serverCode,
    422 errors surfaced in validation messages). Also fixes a latent bug: the client
    matched content type application/json literally, so application/problem+json bodies
    would have been read as text (frontend/src/lib/api/client.ts).
  • Docs: error-handling guide reworked around the new format; every error example across
    5 pages updated to values verified against the actual wire output.

Breaking changes for API consumers:

  1. Error body shape and content type. The detail field survives with identical text, so
    clients reading only detail keep working unchanged.
  2. Validation errors moved 400 -> 422 and now list all errors.
  3. Unhandled errors are JSON instead of plain text.

Verification: backend suite 1763 passed / 2 skipped; the only pre-existing assertions that
needed changes were the intended 400 -> 422 moves. New tests/api/v1/test_error_format.py
pins the contract: body keys and media type, all-errors 422, login special case with
WWW-Authenticate, status-derived fallback codes for plain HTTPException (404 ->
NOT_FOUND), catch-all 500, and /openapi.json containing Problem with no
HTTPValidationError left. ruff, ty, tsc, and oxlint all clean.

Notes:

  • Provider webhook endpoints (Garmin, Polar, Whoop, etc.) intentionally keep plain
    HTTPException - providers only check status codes. Their responses still render as
    problem json via the global handler, with status-derived codes.
  • The open questions from the issue comment default to: RFC 9457 (this PR), validation
    moves to 422, and type omitted - RFC 9457 defines absence as about:blank, so
    resolvable docs URLs can be added later without a breaking change.
  • No timestamp field. A request/trace id extension is the better follow-up if response
    correlation is ever needed.
  • Detail texts keep their pre-existing warts (IntegrityError constraint text, str(e)
    passthroughs) - cleaning those up is a separate concern from the format migration.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0e4458dc-55a6-4e39-aa94-21e8c6dde7b2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

Standardize API Error response format

2 participants