""" Global exception handlers for FastAPI application. This module provides centralized error handling that converts custom exceptions to structured JSON responses with appropriate HTTP status codes. """ import logging import traceback from typing import Any, Dict from fastapi import FastAPI, Request, status from fastapi.responses import JSONResponse from src.server.exceptions import ( AniWorldAPIException, AuthenticationError, AuthorizationError, BadRequestError, ConflictError, NotFoundError, RateLimitError, ValidationError, ) logger = logging.getLogger(__name__) def create_error_response( status_code: int, error: str, message: str, details: Dict[str, Any] | None = None, request_id: str | None = None, ) -> Dict[str, Any]: """ Create standardized error response. Args: status_code: HTTP status code error: Error code/type message: Human-readable error message details: Additional error details request_id: Unique request identifier for tracking Returns: Dictionary containing structured error response """ response = { "success": False, "error": error, "message": message, } if details: response["details"] = details if request_id: response["request_id"] = request_id return response def register_exception_handlers(app: FastAPI) -> None: """ Register all exception handlers with FastAPI app. Args: app: FastAPI application instance """ @app.exception_handler(AuthenticationError) async def authentication_error_handler( request: Request, exc: AuthenticationError ) -> JSONResponse: """Handle authentication errors (401).""" logger.warning( f"Authentication error: {exc.message}", extra={"details": exc.details, "path": str(request.url.path)}, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(AuthorizationError) async def authorization_error_handler( request: Request, exc: AuthorizationError ) -> JSONResponse: """Handle authorization errors (403).""" logger.warning( f"Authorization error: {exc.message}", extra={"details": exc.details, "path": str(request.url.path)}, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(ValidationError) async def validation_error_handler( request: Request, exc: ValidationError ) -> JSONResponse: """Handle validation errors (422).""" logger.info( f"Validation error: {exc.message}", extra={"details": exc.details, "path": str(request.url.path)}, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(BadRequestError) async def bad_request_error_handler( request: Request, exc: BadRequestError ) -> JSONResponse: """Handle bad request errors (400).""" logger.info( f"Bad request error: {exc.message}", extra={"details": exc.details, "path": str(request.url.path)}, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(NotFoundError) async def not_found_error_handler( request: Request, exc: NotFoundError ) -> JSONResponse: """Handle not found errors (404).""" logger.info( f"Not found error: {exc.message}", extra={"details": exc.details, "path": str(request.url.path)}, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(ConflictError) async def conflict_error_handler( request: Request, exc: ConflictError ) -> JSONResponse: """Handle conflict errors (409).""" logger.info( f"Conflict error: {exc.message}", extra={"details": exc.details, "path": str(request.url.path)}, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(RateLimitError) async def rate_limit_error_handler( request: Request, exc: RateLimitError ) -> JSONResponse: """Handle rate limit errors (429).""" logger.warning( f"Rate limit exceeded: {exc.message}", extra={"details": exc.details, "path": str(request.url.path)}, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(AniWorldAPIException) async def api_exception_handler( request: Request, exc: AniWorldAPIException ) -> JSONResponse: """Handle generic API exceptions.""" logger.error( f"API error: {exc.message}", extra={ "error_code": exc.error_code, "details": exc.details, "path": str(request.url.path), }, ) return JSONResponse( status_code=exc.status_code, content=create_error_response( status_code=exc.status_code, error=exc.error_code, message=exc.message, details=exc.details, request_id=getattr(request.state, "request_id", None), ), ) @app.exception_handler(Exception) async def general_exception_handler( request: Request, exc: Exception ) -> JSONResponse: """Handle unexpected exceptions.""" logger.exception( f"Unexpected error: {str(exc)}", extra={"path": str(request.url.path)}, ) # Log full traceback for debugging logger.debug(f"Traceback: {traceback.format_exc()}") # Return generic error response for security return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=create_error_response( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, error="INTERNAL_SERVER_ERROR", message="An unexpected error occurred", request_id=getattr(request.state, "request_id", None), ), )