- 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
258 lines
8.1 KiB
Python
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),
|
|
),
|
|
)
|