""" Global exception handlers and error formatting. """ import logging import traceback from fastapi import FastAPI, Request, status from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException from app.core.config import settings logger = logging.getLogger(__name__) def register_exception_handlers(app: FastAPI): """Register all global exception handlers.""" @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: StarletteHTTPException): """Handle HTTP exceptions with consistent format.""" return JSONResponse( status_code=exc.status_code, content={ "error": True, "status_code": exc.status_code, "detail": exc.detail, "path": str(request.url.path), }, headers=getattr(exc, "headers", None), ) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Handle Pydantic validation errors with readable messages.""" errors = [] for error in exc.errors(): field = " → ".join(str(loc) for loc in error["loc"][1:]) # Skip 'body' errors.append({ "field": field, "message": error["msg"], "type": error["type"], }) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "error": True, "status_code": 422, "detail": "Validation error", "errors": errors, "path": str(request.url.path), }, ) @app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception): """Handle unexpected exceptions.""" error_id = id(exc) logger.error( f"Unhandled exception [{error_id}]: {exc}\n" f"Path: {request.url.path}\n" f"{''.join(traceback.format_exception(type(exc), exc, exc.__traceback__))}" ) # In production, don't leak error details if settings.APP_ENV == "production": detail = "An unexpected error occurred. Please try again later." else: detail = f"{type(exc).__name__}: {str(exc)}" # Report to Sentry if configured try: import sentry_sdk sentry_sdk.capture_exception(exc) except ImportError: pass return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "error": True, "status_code": 500, "detail": detail, "error_id": str(error_id), "path": str(request.url.path), }, )