From ee637b53e0d612248d47f23f8ab8f2056741e2d7 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 24 Aug 2025 09:49:04 +0200 Subject: [PATCH] add excpetion_handlers module --- app/exception handlers/__init__.py | 0 app/exception_handlers/__init__.py | 3 +++ app/exception_handlers/base.py | 34 ++++++++++++++++++++++++ app/exception_handlers/database.py | 24 +++++++++++++++++ app/exception_handlers/registry.py | 11 ++++++++ app/exception_handlers/validation.py | 39 ++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+) delete mode 100644 app/exception handlers/__init__.py create mode 100644 app/exception_handlers/__init__.py create mode 100644 app/exception_handlers/base.py create mode 100644 app/exception_handlers/database.py create mode 100644 app/exception_handlers/registry.py create mode 100644 app/exception_handlers/validation.py diff --git a/app/exception handlers/__init__.py b/app/exception handlers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/exception_handlers/__init__.py b/app/exception_handlers/__init__.py new file mode 100644 index 0000000..e954044 --- /dev/null +++ b/app/exception_handlers/__init__.py @@ -0,0 +1,3 @@ +from app.exception_handlers.registry import register_exception_handlers + +__all__ = ["register_exception_handlers"] \ No newline at end of file diff --git a/app/exception_handlers/base.py b/app/exception_handlers/base.py new file mode 100644 index 0000000..b8229b8 --- /dev/null +++ b/app/exception_handlers/base.py @@ -0,0 +1,34 @@ +# app/exception_handlers/base.py +import orjson +from fastapi import Request +from fastapi.responses import JSONResponse +from rotoger import AppStructLogger + +logger = AppStructLogger().get_logger() + + +class BaseExceptionHandler: + """Base class for all exception handlers with common functionality.""" + + @staticmethod + async def extract_request_info(request: Request): + """Extract common request information.""" + request_path = request.url.path + try: + raw_body = await request.body() + request_body = orjson.loads(raw_body) if raw_body else None + except orjson.JSONDecodeError: + request_body = None + + return request_path, request_body + + @classmethod + async def log_error(cls, message, request_info, **kwargs): + """Log error with standardized format.""" + request_path, request_body = request_info + await logger.aerror( + message, + request_url=request_path, + request_body=request_body, + **kwargs + ) \ No newline at end of file diff --git a/app/exception_handlers/database.py b/app/exception_handlers/database.py new file mode 100644 index 0000000..95062e4 --- /dev/null +++ b/app/exception_handlers/database.py @@ -0,0 +1,24 @@ +from fastapi import Request +from fastapi.responses import JSONResponse +from sqlalchemy.exc import SQLAlchemyError + +from app.exception_handlers.base import BaseExceptionHandler + + +class SQLAlchemyExceptionHandler(BaseExceptionHandler): + """Handles SQLAlchemy database exceptions.""" + + @classmethod + async def handle_exception(cls, request: Request, exc: SQLAlchemyError) -> JSONResponse: + request_info = await cls.extract_request_info(request) + + await cls.log_error( + "Database error occurred", + request_info, + sql_error=repr(exc) + ) + + return JSONResponse( + status_code=500, + content={"message": "A database error occurred. Please try again later."} + ) \ No newline at end of file diff --git a/app/exception_handlers/registry.py b/app/exception_handlers/registry.py new file mode 100644 index 0000000..8de9d45 --- /dev/null +++ b/app/exception_handlers/registry.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from sqlalchemy.exc import SQLAlchemyError +from fastapi.exceptions import ResponseValidationError + +from app.exception_handlers.database import SQLAlchemyExceptionHandler +from app.exception_handlers.validation import ResponseValidationExceptionHandler + +def register_exception_handlers(app: FastAPI) -> None: + """Register all exception handlers with the FastAPI app.""" + app.add_exception_handler(SQLAlchemyError, SQLAlchemyExceptionHandler.handle_exception) + app.add_exception_handler(ResponseValidationError, ResponseValidationExceptionHandler.handle_exception) \ No newline at end of file diff --git a/app/exception_handlers/validation.py b/app/exception_handlers/validation.py new file mode 100644 index 0000000..cfd2a29 --- /dev/null +++ b/app/exception_handlers/validation.py @@ -0,0 +1,39 @@ +from fastapi import Request +from fastapi.exceptions import ResponseValidationError +from fastapi.responses import JSONResponse + +from app.exception_handlers.base import BaseExceptionHandler + + +class ResponseValidationExceptionHandler(BaseExceptionHandler): + """Handles response validation exceptions.""" + + @classmethod + async def handle_exception(cls, request: Request, exc: ResponseValidationError) -> JSONResponse: + request_info = await cls.extract_request_info(request) + errors = exc.errors() + + # Check if this is a None/null response case + is_none_response = False + for error in errors: + if error.get("input") is None and "valid dictionary" in error.get("msg", ""): + is_none_response = True + break + + await cls.log_error( + "Response validation error occurred", + request_info, + validation_errors=errors, + is_none_response=is_none_response + ) + + if is_none_response: + return JSONResponse( + status_code=404, + content={"no_response": "The requested resource was not found"} + ) + else: + return JSONResponse( + status_code=422, + content={"response_format_error": errors} + ) \ No newline at end of file