Files
office_translator/middleware/error_handler.py
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

105 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
from core.logging import get_logger
logger = get_logger(__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,
)