add response_validation_exception_handler

This commit is contained in:
grillazz 2025-08-23 20:04:23 +02:00
parent 8312f06c0b
commit e2f01f89c5
6 changed files with 62 additions and 34 deletions

View File

@ -22,11 +22,12 @@ async def create_nonsense(
@router.get("/", response_model=NonsenseResponse) @router.get("/", response_model=NonsenseResponse)
async def find_nonsense( async def get_nonsense(
name: str, name: str,
db_session: AsyncSession = Depends(get_db), db_session: AsyncSession = Depends(get_db),
): ):
return await Nonsense.find(db_session, name) nonsense = await Nonsense.get_by_name(db_session, name)
return nonsense
@router.delete("/") @router.delete("/")

View File

@ -52,13 +52,8 @@ async def create_stuff(
@router.get("/{name}", response_model=StuffResponse) @router.get("/{name}", response_model=StuffResponse)
async def find_stuff(name: str, db_session: AsyncSession = Depends(get_db)): async def get_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
result = await Stuff.find(db_session, name) result = await Stuff.get_by_name(db_session, name)
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Stuff with name {name} not found.",
)
return result return result

View File

@ -3,6 +3,7 @@ from collections.abc import AsyncGenerator
from rotoger import AppStructLogger from rotoger import AppStructLogger
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from fastapi.exceptions import ResponseValidationError
from app.config import settings as global_settings from app.config import settings as global_settings
@ -26,15 +27,14 @@ AsyncSessionFactory = async_sessionmaker(
# Dependency # Dependency
async def get_db() -> AsyncGenerator: async def get_db() -> AsyncGenerator:
async with AsyncSessionFactory() as session: async with AsyncSessionFactory() as session:
# logger.debug(f"ASYNC Pool: {engine.pool.status()}")
try: try:
yield session yield session
await session.commit() await session.commit()
except Exception as ex: except SQLAlchemyError:
if isinstance(ex, SQLAlchemyError): # Re-raise SQLAlchemy errors to be handled by the global handler
# Re-raise SQLAlchemyError directly without handling
raise raise
else: except Exception as ex:
# Handle other exceptions # Only log actual database-related issues, not response validation
await logger.aerror(f"NonSQLAlchemyError: {repr(ex)}") if not isinstance(ex, ResponseValidationError):
raise # Re-raise after logging await logger.aerror(f"Database-related error: {repr(ex)}")
raise # Re-raise to be handled by appropriate handlers

View File

@ -3,6 +3,7 @@ from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from rotoger import AppStructLogger from rotoger import AppStructLogger
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from fastapi.exceptions import ResponseValidationError
logger = AppStructLogger().get_logger() logger = AppStructLogger().get_logger()
@ -30,6 +31,49 @@ async def sqlalchemy_exception_handler(
) )
async def response_validation_exception_handler(
request: Request, exc: ResponseValidationError
) -> JSONResponse:
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
errors = exc.errors()
# Check if this is a None/null response case
is_none_response = False
for error in errors:
# Check for null input pattern
if error.get("input") is None and "valid dictionary" in error.get("msg", ""):
is_none_response = True
break
await logger.aerror(
"Response validation error occurred",
validation_errors=errors,
request_url=request_path,
request_body=request_body,
is_none_response=is_none_response,
)
if is_none_response:
# Return 404 when response is None (resource not found)
return JSONResponse(
status_code=404,
content={"no_response": "The requested resource was not found"},
)
else:
# Return 422 when response exists but doesn't match expected format
return JSONResponse(
status_code=422,
content={"response_format_error": errors},
)
def register_exception_handlers(app: FastAPI) -> None: def register_exception_handlers(app: FastAPI) -> None:
"""Register all exception handlers with the FastAPI app.""" """Register all exception handlers with the FastAPI app."""
app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler) app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler)
app.add_exception_handler(ResponseValidationError, response_validation_exception_handler)

View File

@ -20,22 +20,10 @@ class Nonsense(Base):
# TODO: apply relation to other tables # TODO: apply relation to other tables
@classmethod @classmethod
async def find(cls, db_session: AsyncSession, name: str): async def get_by_name(cls, db_session: AsyncSession, name: str):
"""
:param db_session:
:param name:
:return:
"""
stmt = select(cls).where(cls.name == name) stmt = select(cls).where(cls.name == name)
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
instance = result.scalars().first() instance = result.scalars().first()
if instance is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"Record not found": f"There is no record for requested name value : {name}"
},
)
else:
return instance return instance

View File

@ -35,7 +35,7 @@ class Stuff(Base):
@classmethod @classmethod
@compile_sql_or_scalar @compile_sql_or_scalar
async def find(cls, db_session: AsyncSession, name: str, compile_sql=False): async def get_by_name(cls, db_session: AsyncSession, name: str, compile_sql=False):
stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name) stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name)
return stmt return stmt