from typing import Any from fastapi import FastAPI, Request from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException class AppError(Exception): status_code: int = 400 code: str = "bad_request" def __init__(self, message: str, *, details: dict[str, Any] | None = None): self.message = message self.details = details or {} super().__init__(message) class NotFoundError(AppError): status_code, code = 404, "not_found" class ConflictError(AppError): status_code, code = 409, "conflict" class UnauthorizedError(AppError): status_code, code = 401, "unauthorized" class ForbiddenError(AppError): status_code, code = 403, "forbidden" class DomainValidationError(AppError): status_code, code = 422, "validation_error" class PayloadTooLargeError(AppError): status_code, code = 413, "payload_too_large" _HTTP_CODES = { 400: "bad_request", 401: "unauthorized", 403: "forbidden", 404: "not_found", 405: "method_not_allowed", 409: "conflict", 413: "payload_too_large", 422: "validation_error", 500: "internal_error", } def _payload(code: str, message: str, details: Any = None) -> dict[str, Any]: return {"error": {"code": code, "message": message, "details": details or {}}} def register_error_handlers(app: FastAPI) -> None: @app.exception_handler(AppError) async def app_error_handler(_: Request, exc: AppError) -> JSONResponse: return JSONResponse( status_code=exc.status_code, content=_payload(exc.code, exc.message, exc.details), ) @app.exception_handler(RequestValidationError) async def validation_handler( _: Request, exc: RequestValidationError ) -> JSONResponse: return JSONResponse( status_code=422, content=_payload( "validation_error", "Les données envoyées sont invalides.", {"errors": jsonable_encoder(exc.errors())}, ), ) @app.exception_handler(StarletteHTTPException) async def http_exception_handler( _: Request, exc: StarletteHTTPException ) -> JSONResponse: # Normalize framework-raised HTTP errors (404 route, 405…) to the # single error shape of §8.3. code = _HTTP_CODES.get(exc.status_code, "error") message = ( exc.detail if isinstance(exc.detail, str) else "Une erreur est survenue." ) return JSONResponse( status_code=exc.status_code, content=_payload(code, message) )