Skip to content

Commit ee637b5

Browse files
committed
add excpetion_handlers module
1 parent 71d90aa commit ee637b5

File tree

6 files changed

+111
-0
lines changed

6 files changed

+111
-0
lines changed

app/exception handlers/__init__.py

Whitespace-only changes.

app/exception_handlers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from app.exception_handlers.registry import register_exception_handlers
2+
3+
__all__ = ["register_exception_handlers"]

app/exception_handlers/base.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# app/exception_handlers/base.py
2+
import orjson
3+
from fastapi import Request
4+
from fastapi.responses import JSONResponse
5+
from rotoger import AppStructLogger
6+
7+
logger = AppStructLogger().get_logger()
8+
9+
10+
class BaseExceptionHandler:
11+
"""Base class for all exception handlers with common functionality."""
12+
13+
@staticmethod
14+
async def extract_request_info(request: Request):
15+
"""Extract common request information."""
16+
request_path = request.url.path
17+
try:
18+
raw_body = await request.body()
19+
request_body = orjson.loads(raw_body) if raw_body else None
20+
except orjson.JSONDecodeError:
21+
request_body = None
22+
23+
return request_path, request_body
24+
25+
@classmethod
26+
async def log_error(cls, message, request_info, **kwargs):
27+
"""Log error with standardized format."""
28+
request_path, request_body = request_info
29+
await logger.aerror(
30+
message,
31+
request_url=request_path,
32+
request_body=request_body,
33+
**kwargs
34+
)

app/exception_handlers/database.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from fastapi import Request
2+
from fastapi.responses import JSONResponse
3+
from sqlalchemy.exc import SQLAlchemyError
4+
5+
from app.exception_handlers.base import BaseExceptionHandler
6+
7+
8+
class SQLAlchemyExceptionHandler(BaseExceptionHandler):
9+
"""Handles SQLAlchemy database exceptions."""
10+
11+
@classmethod
12+
async def handle_exception(cls, request: Request, exc: SQLAlchemyError) -> JSONResponse:
13+
request_info = await cls.extract_request_info(request)
14+
15+
await cls.log_error(
16+
"Database error occurred",
17+
request_info,
18+
sql_error=repr(exc)
19+
)
20+
21+
return JSONResponse(
22+
status_code=500,
23+
content={"message": "A database error occurred. Please try again later."}
24+
)

app/exception_handlers/registry.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import FastAPI
2+
from sqlalchemy.exc import SQLAlchemyError
3+
from fastapi.exceptions import ResponseValidationError
4+
5+
from app.exception_handlers.database import SQLAlchemyExceptionHandler
6+
from app.exception_handlers.validation import ResponseValidationExceptionHandler
7+
8+
def register_exception_handlers(app: FastAPI) -> None:
9+
"""Register all exception handlers with the FastAPI app."""
10+
app.add_exception_handler(SQLAlchemyError, SQLAlchemyExceptionHandler.handle_exception)
11+
app.add_exception_handler(ResponseValidationError, ResponseValidationExceptionHandler.handle_exception)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from fastapi import Request
2+
from fastapi.exceptions import ResponseValidationError
3+
from fastapi.responses import JSONResponse
4+
5+
from app.exception_handlers.base import BaseExceptionHandler
6+
7+
8+
class ResponseValidationExceptionHandler(BaseExceptionHandler):
9+
"""Handles response validation exceptions."""
10+
11+
@classmethod
12+
async def handle_exception(cls, request: Request, exc: ResponseValidationError) -> JSONResponse:
13+
request_info = await cls.extract_request_info(request)
14+
errors = exc.errors()
15+
16+
# Check if this is a None/null response case
17+
is_none_response = False
18+
for error in errors:
19+
if error.get("input") is None and "valid dictionary" in error.get("msg", ""):
20+
is_none_response = True
21+
break
22+
23+
await cls.log_error(
24+
"Response validation error occurred",
25+
request_info,
26+
validation_errors=errors,
27+
is_none_response=is_none_response
28+
)
29+
30+
if is_none_response:
31+
return JSONResponse(
32+
status_code=404,
33+
content={"no_response": "The requested resource was not found"}
34+
)
35+
else:
36+
return JSONResponse(
37+
status_code=422,
38+
content={"response_format_error": errors}
39+
)

0 commit comments

Comments
 (0)