Files
TJWaterServerBinary/app/api/problem_details.py
T

90 lines
2.5 KiB
Python

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"),
)