diff --git a/app/api/nonsense.py b/app/api/nonsense.py index 0c77a06..869aab6 100644 --- a/app/api/nonsense.py +++ b/app/api/nonsense.py @@ -22,11 +22,12 @@ async def create_nonsense( @router.get("/", response_model=NonsenseResponse) -async def find_nonsense( +async def get_nonsense( name: str, 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("/") diff --git a/app/api/stuff.py b/app/api/stuff.py index 83fedf3..d36bcd2 100644 --- a/app/api/stuff.py +++ b/app/api/stuff.py @@ -52,13 +52,8 @@ async def create_stuff( @router.get("/{name}", response_model=StuffResponse) -async def find_stuff(name: str, db_session: AsyncSession = Depends(get_db)): - result = await Stuff.find(db_session, name) - if not result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Stuff with name {name} not found.", - ) +async def get_stuff(name: str, db_session: AsyncSession = Depends(get_db)): + result = await Stuff.get_by_name(db_session, name) return result @@ -89,7 +84,7 @@ async def find_stuff_pool( HTTPException: If the 'Stuff' object is not found or an SQLAlchemyError occurs. """ 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)) except SQLAlchemyError as ex: raise HTTPException( @@ -105,7 +100,7 @@ async def find_stuff_pool( @router.delete("/{name}") 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) @@ -115,6 +110,6 @@ async def update_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) await stuff.update(**payload.model_dump()) return stuff diff --git a/app/database.py b/app/database.py index 8e00277..ea56a86 100644 --- a/app/database.py +++ b/app/database.py @@ -1,5 +1,6 @@ from collections.abc import AsyncGenerator +from fastapi.exceptions import ResponseValidationError from rotoger import AppStructLogger from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine @@ -26,15 +27,14 @@ AsyncSessionFactory = async_sessionmaker( # Dependency async def get_db() -> AsyncGenerator: async with AsyncSessionFactory() as session: - # logger.debug(f"ASYNC Pool: {engine.pool.status()}") try: yield session await session.commit() + except SQLAlchemyError: + # Re-raise SQLAlchemy errors to be handled by the global handler + raise except Exception as ex: - if isinstance(ex, SQLAlchemyError): - # Re-raise SQLAlchemyError directly without handling - raise - else: - # Handle other exceptions - await logger.aerror(f"NonSQLAlchemyError: {repr(ex)}") - raise # Re-raise after logging + # Only log actual database-related issues, not response validation + if not isinstance(ex, ResponseValidationError): + await logger.aerror(f"Database-related error: {repr(ex)}") + raise # Re-raise to be handled by appropriate handlers diff --git a/app/exception_handlers.py b/app/exception_handlers.py index 492556e..6298a3e 100644 --- a/app/exception_handlers.py +++ b/app/exception_handlers.py @@ -1,12 +1,14 @@ import orjson from fastapi import FastAPI, Request +from fastapi.exceptions import ResponseValidationError from fastapi.responses import JSONResponse from rotoger import AppStructLogger from sqlalchemy.exc import SQLAlchemyError 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( request: Request, exc: SQLAlchemyError ) -> 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: """Register all exception handlers with the FastAPI app.""" app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler) + app.add_exception_handler( + ResponseValidationError, response_validation_exception_handler + ) diff --git a/app/models/nonsense.py b/app/models/nonsense.py index a70a067..65f79dc 100644 --- a/app/models/nonsense.py +++ b/app/models/nonsense.py @@ -1,6 +1,5 @@ import uuid -from fastapi import HTTPException, status from sqlalchemy import String, select from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.asyncio import AsyncSession @@ -20,22 +19,8 @@ class Nonsense(Base): # TODO: apply relation to other tables @classmethod - async def find(cls, db_session: AsyncSession, name: str): - """ - - :param db_session: - :param name: - :return: - """ + async def get_by_name(cls, db_session: AsyncSession, name: str): stmt = select(cls).where(cls.name == name) result = await db_session.execute(stmt) 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 diff --git a/app/models/stuff.py b/app/models/stuff.py index 1edf5c1..d4d5e1d 100644 --- a/app/models/stuff.py +++ b/app/models/stuff.py @@ -35,7 +35,7 @@ class Stuff(Base): @classmethod @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) return stmt