Files
orion/app/exceptions/handler.py

208 lines
6.4 KiB
Python

# app/exceptions/handler.py
"""
Exception handler for FastAPI application.
Provides consistent error responses and logging for all custom exceptions.
"""
import logging
from typing import Union
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from .base import LetzShopException
logger = logging.getLogger(__name__)
class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
"""Middleware to handle custom exceptions and convert them to JSON responses."""
async def dispatch(self, request: Request, call_next):
try:
response = await call_next(request)
return response
except LetzShopException as exc:
return await self.handle_custom_exception(request, exc)
except HTTPException as exc:
return await self.handle_http_exception(request, exc)
except Exception as exc:
return await self.handle_generic_exception(request, exc)
async def handle_custom_exception(
self,
request: Request,
exc: LetzShopException
) -> JSONResponse:
"""Handle custom LetzShop exceptions."""
# Log the exception
logger.error(
f"Custom exception in {request.method} {request.url}: "
f"{exc.error_code} - {exc.message}",
extra={
"error_code": exc.error_code,
"status_code": exc.status_code,
"details": exc.details,
"url": str(request.url),
"method": request.method,
}
)
return JSONResponse(
status_code=exc.status_code,
content=exc.to_dict()
)
async def handle_http_exception(
self,
request: Request,
exc: HTTPException
) -> JSONResponse:
"""Handle FastAPI HTTPExceptions."""
logger.error(
f"HTTP exception in {request.method} {request.url}: "
f"{exc.status_code} - {exc.detail}",
extra={
"status_code": exc.status_code,
"detail": exc.detail,
"url": str(request.url),
"method": request.method,
}
)
return JSONResponse(
status_code=exc.status_code,
content={
"error_code": f"HTTP_{exc.status_code}",
"message": exc.detail,
"status_code": exc.status_code,
}
)
async def handle_generic_exception(
self,
request: Request,
exc: Exception
) -> JSONResponse:
"""Handle unexpected exceptions."""
logger.error(
f"Unexpected exception in {request.method} {request.url}: {str(exc)}",
exc_info=True,
extra={
"url": str(request.url),
"method": request.method,
"exception_type": type(exc).__name__,
}
)
return JSONResponse(
status_code=500,
content={
"error_code": "INTERNAL_SERVER_ERROR",
"message": "Internal server error",
"status_code": 500,
}
)
def setup_exception_handlers(app):
"""Setup exception handlers for the FastAPI app."""
@app.exception_handler(LetzShopException)
async def custom_exception_handler(request: Request, exc: LetzShopException):
"""Handle custom LetzShop exceptions."""
logger.error(
f"Custom exception in {request.method} {request.url}: "
f"{exc.error_code} - {exc.message}",
extra={
"error_code": exc.error_code,
"status_code": exc.status_code,
"details": exc.details,
"url": str(request.url),
"method": request.method,
}
)
return JSONResponse(
status_code=exc.status_code,
content=exc.to_dict()
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""Handle FastAPI HTTPExceptions with consistent format."""
logger.error(
f"HTTP exception in {request.method} {request.url}: "
f"{exc.status_code} - {exc.detail}",
extra={
"status_code": exc.status_code,
"detail": exc.detail,
"url": str(request.url),
"method": request.method,
}
)
return JSONResponse(
status_code=exc.status_code,
content={
"error_code": f"HTTP_{exc.status_code}",
"message": exc.detail,
"status_code": exc.status_code,
}
)
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
"""Handle unexpected exceptions."""
logger.error(
f"Unexpected exception in {request.method} {request.url}: {str(exc)}",
exc_info=True,
extra={
"url": str(request.url),
"method": request.method,
"exception_type": type(exc).__name__,
}
)
return JSONResponse(
status_code=500,
content={
"error_code": "INTERNAL_SERVER_ERROR",
"message": "Internal server error",
"status_code": 500,
}
)
# Utility functions for common exception scenarios
def raise_not_found(resource_type: str, identifier: str) -> None:
"""Convenience function to raise ResourceNotFoundException."""
from .base import ResourceNotFoundException
raise ResourceNotFoundException(resource_type, identifier)
def raise_validation_error(message: str, field: str = None, details: dict = None) -> None:
"""Convenience function to raise ValidationException."""
from .base import ValidationException
raise ValidationException(message, field, details)
def raise_auth_error(message: str = "Authentication failed") -> None:
"""Convenience function to raise AuthenticationException."""
from .base import AuthenticationException
raise AuthenticationException(message)
def raise_permission_error(message: str = "Access denied") -> None:
"""Convenience function to raise AuthorizationException."""
from .base import AuthorizationException
raise AuthorizationException(message)