""" Global Error Handling Middleware Catches all unhandled exceptions and standardizes API error responses. """ import logging from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response, JSONResponse from fastapi import HTTPException from starlette.exceptions import HTTPException as StarletteHTTPException # Import APIKeyError for handling from middleware.api_key_auth import APIKeyError try: import structlog logger = structlog.get_logger(__name__) except ImportError: logger = logging.getLogger(__name__) def format_error_response( status_code: int, message: str, error_code: str = None, details: dict = None, request_id: str = "unknown", headers: dict = None, ) -> JSONResponse: """ Standardizes the error response format. Format: {error: "CODE", message: "...", details: {...}} """ if not error_code: error_code = _map_http_status_to_code(status_code) content = {"error": error_code, "message": message, "details": details or {}} # Always include request_id in details if not present if "request_id" not in content["details"]: content["details"]["request_id"] = request_id return JSONResponse(status_code=status_code, content=content, headers=headers) def _map_http_status_to_code(status_code: int) -> str: """Map HTTP status codes to architectural error codes.""" mapping = { 400: "INVALID_FORMAT", 401: "UNAUTHORIZED", 403: "FORBIDDEN", 404: "NOT_FOUND", 405: "METHOD_NOT_ALLOWED", 413: "FILE_TOO_LARGE", 422: "VALIDATION_ERROR", 429: "QUOTA_EXCEEDED", 502: "PROVIDER_ERROR", 503: "SERVICE_UNAVAILABLE", } return mapping.get(status_code, "INTERNAL_ERROR") class ErrorHandlingMiddleware(BaseHTTPMiddleware): """ Catch all unhandled exceptions (Exception) that bubble up to the top. Note: HTTPException is often caught by FastAPI handlers before reaching here. """ async def dispatch(self, request: Request, call_next) -> Response: try: return await call_next(request) except APIKeyError as e: # Handle APIKeyError with structured response using to_response() request_id = getattr(request.state, "request_id", "unknown") logger.info(f"[{request_id}] API Key authentication error: {e.code}") return e.to_response() except Exception as e: request_id = getattr(request.state, "request_id", "unknown") # If it's already an HTTPException, we might want to handle it specifically if it leaked through if isinstance(e, (HTTPException, StarletteHTTPException)): detail = e.detail if hasattr(e, "detail") and e.detail else {} if isinstance(detail, dict): return format_error_response( status_code=e.status_code, message=detail.get("message", "Une erreur s'est produite."), error_code=detail.get("error"), request_id=request_id, ) return format_error_response( status_code=e.status_code, message=str(detail) if detail else "Une erreur s'est produite.", request_id=request_id, ) # Log the full stack trace for internal debugging logger.exception(f"[{request_id}] Unhandled internal exception: {str(e)}") # Return generic error in French to user (AC4, AC5) return format_error_response( status_code=500, message="Une erreur inattendue s'est produite. Veuillez réessayer plus tard.", error_code="INTERNAL_ERROR", request_id=request_id, )