Files
office_translator/middleware/error_handler.py
2026-03-07 11:42:58 +01:00

108 lines
3.8 KiB
Python

"""
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,
)