from __future__ import annotations from typing import Any from uuid import uuid4 from fastapi import FastAPI, HTTPException, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import BaseModel, Field class ProblemDetails(BaseModel): """RFC 9457 compatible error response used by the REST contract.""" type: str title: str status: int detail: str instance: str code: str trace_id: str errors: list[dict[str, Any]] = Field(default_factory=list) def _trace_id(request: Request) -> str: return request.headers.get("X-Request-Id") or str(uuid4()) def _problem_response( request: Request, *, status_code: int, title: str, detail: str, code: str, errors: list[dict[str, Any]] | None = None, ) -> JSONResponse: problem = ProblemDetails( type=f"https://tjwater.example/problems/{code.replace('_', '-')}", title=title, status=status_code, detail=detail, instance=request.url.path, code=code, trace_id=_trace_id(request), errors=errors or [], ) return JSONResponse( status_code=status_code, content=problem.model_dump(mode="json"), media_type="application/problem+json", ) def install_problem_details_handlers(app: FastAPI) -> None: @app.exception_handler(RequestValidationError) async def validation_error_handler( request: Request, exc: RequestValidationError, ) -> JSONResponse: return _problem_response( request, status_code=422, title="Validation error", detail="Request validation failed", code="validation_error", errors=exc.errors(), ) @app.exception_handler(HTTPException) async def http_error_handler(request: Request, exc: HTTPException) -> JSONResponse: detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail) code_by_status = { 401: "unauthenticated", 403: "forbidden", 404: "not_found", 409: "conflict", 422: "validation_error", 503: "dependency_unavailable", } return _problem_response( request, status_code=exc.status_code, title=code_by_status.get(exc.status_code, "request_error") .replace("_", " ") .title(), detail=detail, code=code_by_status.get(exc.status_code, "request_error"), )