from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException from .logging_config import get_logger, request_id_var from .schemas import ErrorBody, ErrorCode, ErrorResponse log = get_logger("monlet.errors") def _request_id(request: Request) -> str: rid = getattr(request.state, "request_id", None) if rid is None: rid = request_id_var.get() or "" return rid def error_response( request: Request, status_code: int, code: ErrorCode, message: str ) -> JSONResponse: body = ErrorResponse( error=ErrorBody(code=code, message=message, request_id=_request_id(request)) ) return JSONResponse(status_code=status_code, content=body.model_dump()) _STATUS_TO_CODE: dict[int, ErrorCode] = { 400: "validation", 401: "unauthorized", 404: "not_found", 413: "payload_too_large", 429: "rate_limited", 500: "internal", 503: "not_ready", } def install_error_handlers(app: FastAPI) -> None: @app.exception_handler(RequestValidationError) async def _validation(request: Request, exc: RequestValidationError): message = "request validation failed" return error_response(request, 400, "validation", message) @app.exception_handler(StarletteHTTPException) async def _http(request: Request, exc: StarletteHTTPException): code = _STATUS_TO_CODE.get(exc.status_code, "internal") msg = exc.detail if isinstance(exc.detail, str) else code return error_response(request, exc.status_code, code, msg) @app.exception_handler(Exception) async def _unhandled(request: Request, exc: Exception): log.exception("unhandled_exception", error=type(exc).__name__) return error_response(request, 500, "internal", "internal server error")