Aniworld/src/server/middleware/error_handler.py
Lukas 27108aacda Fix architecture issues from todolist
- Add documentation warnings for in-memory rate limiting and failed login attempts
- Consolidate duplicate health endpoints into api/health.py
- Fix CLI to use correct async rescan method names
- Update download.py and anime.py to use custom exception classes
- Add WebSocket room validation and rate limiting
2025-12-15 14:23:41 +01:00

258 lines
8.1 KiB
Python

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