Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import logging | |
| from http import HTTPStatus | |
| from typing import Any | |
| from fastapi import FastAPI, Request, status | |
| from fastapi.encoders import jsonable_encoder | |
| from fastapi.exceptions import RequestValidationError | |
| from starlette.exceptions import HTTPException as StarletteHTTPException | |
| from starlette.responses import JSONResponse | |
| from backend.core.config import settings | |
| from backend.core.correlation import get_request_id | |
| logger = logging.getLogger(__name__) | |
| class AppException(Exception): | |
| def __init__( | |
| self, | |
| *, | |
| code: str, | |
| message: str, | |
| status_code: int = status.HTTP_400_BAD_REQUEST, | |
| details: Any | None = None, | |
| ) -> None: | |
| self.code = code | |
| self.message = message | |
| self.status_code = status_code | |
| self.details = details | |
| super().__init__(message) | |
| class ResourceNotFound(AppException): | |
| def __init__(self, resource: str, identifier: Any): | |
| super().__init__( | |
| code="not_found", | |
| message=f"{resource} not found: {identifier}", | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| ) | |
| # RFC 9110 name; Python 3.13+ exposes `UNPROCESSABLE_CONTENT` (422). | |
| _VALIDATION_STATUS = getattr(HTTPStatus, "UNPROCESSABLE_CONTENT", HTTPStatus.UNPROCESSABLE_ENTITY) | |
| def _error_payload( | |
| *, | |
| code: str, | |
| message: str, | |
| request_id: str | None, | |
| details: Any | None = None, | |
| ) -> dict[str, Any]: | |
| body: dict[str, Any] = { | |
| "error": { | |
| "code": code, | |
| "message": message, | |
| "request_id": request_id, | |
| }, | |
| } | |
| if details is not None: | |
| body["details"] = jsonable_encoder(details) | |
| return body | |
| def register_exception_handlers(app: FastAPI) -> None: | |
| async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse: | |
| rid = get_request_id() | |
| return JSONResponse( | |
| status_code=exc.status_code, | |
| content=_error_payload( | |
| code=exc.code, | |
| message=exc.message, | |
| request_id=rid, | |
| details=exc.details, | |
| ), | |
| ) | |
| async def http_exception_handler( | |
| request: Request, | |
| exc: StarletteHTTPException, | |
| ) -> JSONResponse: | |
| rid = get_request_id() | |
| detail = exc.detail | |
| if isinstance(detail, str): | |
| message = detail | |
| elif isinstance(detail, list): | |
| message = "Request failed" | |
| else: | |
| message = str(detail) | |
| return JSONResponse( | |
| status_code=exc.status_code, | |
| content=_error_payload( | |
| code="http_error", | |
| message=message, | |
| request_id=rid, | |
| ), | |
| ) | |
| async def validation_exception_handler( | |
| request: Request, | |
| exc: RequestValidationError, | |
| ) -> JSONResponse: | |
| rid = get_request_id() | |
| return JSONResponse( | |
| status_code=_VALIDATION_STATUS, | |
| content=_error_payload( | |
| code="validation_error", | |
| message="Request validation failed", | |
| request_id=rid, | |
| details=exc.errors(), | |
| ), | |
| ) | |
| async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: | |
| rid = get_request_id() | |
| logger.exception("Unhandled error (request_id=%s)", rid) | |
| if settings.app_env == "production": | |
| message = "Internal server error" | |
| else: | |
| message = str(exc) | |
| return JSONResponse( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| content=_error_payload( | |
| code="internal_error", | |
| message=message, | |
| request_id=rid, | |
| ), | |
| ) | |