feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
107
middleware/error_handler.py
Normal file
107
middleware/error_handler.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user