Skip to content

Commit 5ceda55

Browse files
authored
Merge pull request #215 from grillazz/12-add-json-field-example
add more exception handlers
2 parents 4635b4e + 7d122f7 commit 5ceda55

File tree

6 files changed

+67
-39
lines changed

6 files changed

+67
-39
lines changed

app/api/nonsense.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ async def create_nonsense(
2222

2323

2424
@router.get("/", response_model=NonsenseResponse)
25-
async def find_nonsense(
25+
async def get_nonsense(
2626
name: str,
2727
db_session: AsyncSession = Depends(get_db),
2828
):
29-
return await Nonsense.find(db_session, name)
29+
nonsense = await Nonsense.get_by_name(db_session, name)
30+
return nonsense
3031

3132

3233
@router.delete("/")

app/api/stuff.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,8 @@ async def create_stuff(
5252

5353

5454
@router.get("/{name}", response_model=StuffResponse)
55-
async def find_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
56-
result = await Stuff.find(db_session, name)
57-
if not result:
58-
raise HTTPException(
59-
status_code=status.HTTP_404_NOT_FOUND,
60-
detail=f"Stuff with name {name} not found.",
61-
)
55+
async def get_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
56+
result = await Stuff.get_by_name(db_session, name)
6257
return result
6358

6459

@@ -89,7 +84,7 @@ async def find_stuff_pool(
8984
HTTPException: If the 'Stuff' object is not found or an SQLAlchemyError occurs.
9085
"""
9186
try:
92-
stmt = await Stuff.find(db_session, name, compile_sql=True)
87+
stmt = await Stuff.get_by_name(db_session, name, compile_sql=True)
9388
result = await request.app.postgres_pool.fetchrow(str(stmt))
9489
except SQLAlchemyError as ex:
9590
raise HTTPException(
@@ -105,7 +100,7 @@ async def find_stuff_pool(
105100

106101
@router.delete("/{name}")
107102
async def delete_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
108-
stuff = await Stuff.find(db_session, name)
103+
stuff = await Stuff.get_by_name(db_session, name)
109104
return await Stuff.delete(stuff, db_session)
110105

111106

@@ -115,6 +110,6 @@ async def update_stuff(
115110
name: str,
116111
db_session: AsyncSession = Depends(get_db),
117112
):
118-
stuff = await Stuff.find(db_session, name)
113+
stuff = await Stuff.get_by_name(db_session, name)
119114
await stuff.update(**payload.model_dump())
120115
return stuff

app/database.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections.abc import AsyncGenerator
22

3+
from fastapi.exceptions import ResponseValidationError
34
from rotoger import AppStructLogger
45
from sqlalchemy.exc import SQLAlchemyError
56
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
@@ -26,15 +27,14 @@
2627
# Dependency
2728
async def get_db() -> AsyncGenerator:
2829
async with AsyncSessionFactory() as session:
29-
# logger.debug(f"ASYNC Pool: {engine.pool.status()}")
3030
try:
3131
yield session
3232
await session.commit()
33+
except SQLAlchemyError:
34+
# Re-raise SQLAlchemy errors to be handled by the global handler
35+
raise
3336
except Exception as ex:
34-
if isinstance(ex, SQLAlchemyError):
35-
# Re-raise SQLAlchemyError directly without handling
36-
raise
37-
else:
38-
# Handle other exceptions
39-
await logger.aerror(f"NonSQLAlchemyError: {repr(ex)}")
40-
raise # Re-raise after logging
37+
# Only log actual database-related issues, not response validation
38+
if not isinstance(ex, ResponseValidationError):
39+
await logger.aerror(f"Database-related error: {repr(ex)}")
40+
raise # Re-raise to be handled by appropriate handlers

app/exception_handlers.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import orjson
22
from fastapi import FastAPI, Request
3+
from fastapi.exceptions import ResponseValidationError
34
from fastapi.responses import JSONResponse
45
from rotoger import AppStructLogger
56
from sqlalchemy.exc import SQLAlchemyError
67

78
logger = AppStructLogger().get_logger()
89

9-
#TODO: add reasoning for this in readme plus higligh using re-raise in db session
10+
11+
# TODO: add reasoning for this in readme plus higligh using re-raise in db session
1012
async def sqlalchemy_exception_handler(
1113
request: Request, exc: SQLAlchemyError
1214
) -> JSONResponse:
@@ -30,6 +32,51 @@ async def sqlalchemy_exception_handler(
3032
)
3133

3234

35+
async def response_validation_exception_handler(
36+
request: Request, exc: ResponseValidationError
37+
) -> JSONResponse:
38+
request_path = request.url.path
39+
try:
40+
raw_body = await request.body()
41+
request_body = orjson.loads(raw_body) if raw_body else None
42+
except orjson.JSONDecodeError:
43+
request_body = None
44+
45+
errors = exc.errors()
46+
47+
# Check if this is a None/null response case
48+
is_none_response = False
49+
for error in errors:
50+
# Check for null input pattern
51+
if error.get("input") is None and "valid dictionary" in error.get("msg", ""):
52+
is_none_response = True
53+
break
54+
55+
await logger.aerror(
56+
"Response validation error occurred",
57+
validation_errors=errors,
58+
request_url=request_path,
59+
request_body=request_body,
60+
is_none_response=is_none_response,
61+
)
62+
63+
if is_none_response:
64+
# Return 404 when response is None (resource not found)
65+
return JSONResponse(
66+
status_code=404,
67+
content={"no_response": "The requested resource was not found"},
68+
)
69+
else:
70+
# Return 422 when response exists but doesn't match expected format
71+
return JSONResponse(
72+
status_code=422,
73+
content={"response_format_error": errors},
74+
)
75+
76+
3377
def register_exception_handlers(app: FastAPI) -> None:
3478
"""Register all exception handlers with the FastAPI app."""
3579
app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler)
80+
app.add_exception_handler(
81+
ResponseValidationError, response_validation_exception_handler
82+
)

app/models/nonsense.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import uuid
22

3-
from fastapi import HTTPException, status
43
from sqlalchemy import String, select
54
from sqlalchemy.dialects.postgresql import UUID
65
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,22 +19,8 @@ class Nonsense(Base):
2019
# TODO: apply relation to other tables
2120

2221
@classmethod
23-
async def find(cls, db_session: AsyncSession, name: str):
24-
"""
25-
26-
:param db_session:
27-
:param name:
28-
:return:
29-
"""
22+
async def get_by_name(cls, db_session: AsyncSession, name: str):
3023
stmt = select(cls).where(cls.name == name)
3124
result = await db_session.execute(stmt)
3225
instance = result.scalars().first()
33-
if instance is None:
34-
raise HTTPException(
35-
status_code=status.HTTP_404_NOT_FOUND,
36-
detail={
37-
"Record not found": f"There is no record for requested name value : {name}"
38-
},
39-
)
40-
else:
41-
return instance
26+
return instance

app/models/stuff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Stuff(Base):
3535

3636
@classmethod
3737
@compile_sql_or_scalar
38-
async def find(cls, db_session: AsyncSession, name: str, compile_sql=False):
38+
async def get_by_name(cls, db_session: AsyncSession, name: str, compile_sql=False):
3939
stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name)
4040
return stmt
4141

0 commit comments

Comments
 (0)