Merge pull request #215 from grillazz/12-add-json-field-example

add more exception handlers
This commit is contained in:
Ordinary Hobbit 2025-08-24 09:35:57 +02:00 committed by GitHub
commit 5ceda55d6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 39 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
@ -89,7 +84,7 @@ async def find_stuff_pool(
HTTPException: If the 'Stuff' object is not found or an SQLAlchemyError occurs. HTTPException: If the 'Stuff' object is not found or an SQLAlchemyError occurs.
""" """
try: try:
stmt = await Stuff.find(db_session, name, compile_sql=True) stmt = await Stuff.get_by_name(db_session, name, compile_sql=True)
result = await request.app.postgres_pool.fetchrow(str(stmt)) result = await request.app.postgres_pool.fetchrow(str(stmt))
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException( raise HTTPException(
@ -105,7 +100,7 @@ async def find_stuff_pool(
@router.delete("/{name}") @router.delete("/{name}")
async def delete_stuff(name: str, db_session: AsyncSession = Depends(get_db)): async def delete_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
stuff = await Stuff.find(db_session, name) stuff = await Stuff.get_by_name(db_session, name)
return await Stuff.delete(stuff, db_session) return await Stuff.delete(stuff, db_session)
@ -115,6 +110,6 @@ async def update_stuff(
name: str, name: str,
db_session: AsyncSession = Depends(get_db), db_session: AsyncSession = Depends(get_db),
): ):
stuff = await Stuff.find(db_session, name) stuff = await Stuff.get_by_name(db_session, name)
await stuff.update(**payload.model_dump()) await stuff.update(**payload.model_dump())
return stuff return stuff

View File

@ -1,5 +1,6 @@
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from fastapi.exceptions import ResponseValidationError
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
@ -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

@ -1,12 +1,14 @@
import orjson import orjson
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.exceptions import ResponseValidationError
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
logger = AppStructLogger().get_logger() logger = AppStructLogger().get_logger()
#TODO: add reasoning for this in readme plus higligh using re-raise in db session
# TODO: add reasoning for this in readme plus higligh using re-raise in db session
async def sqlalchemy_exception_handler( async def sqlalchemy_exception_handler(
request: Request, exc: SQLAlchemyError request: Request, exc: SQLAlchemyError
) -> JSONResponse: ) -> JSONResponse:
@ -30,6 +32,51 @@ 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

@ -1,6 +1,5 @@
import uuid import uuid
from fastapi import HTTPException, status
from sqlalchemy import String, select from sqlalchemy import String, select
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -20,22 +19,8 @@ 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