Skip to content

Commit a0680a5

Browse files
committed
KIT-4352 Add validation exception handling
1 parent e3b8df5 commit a0680a5

File tree

5 files changed

+59
-4
lines changed

5 files changed

+59
-4
lines changed

.idea/sag_py_web_common.iml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
asgi-logger>=0.1.0
2-
fastapi>=0.115.12, <1
2+
fastapi>=0.115.12, <1
3+
starlette>=0.46.2
4+

sag_py_web_common/json_exception_handler.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
from logging import Logger
33
from typing import Any
44

5-
from fastapi.encoders import jsonable_encoder
65
from fastapi.exception_handlers import http_exception_handler
6+
from fastapi.exceptions import RequestValidationError
77
from fastapi.responses import JSONResponse
88
from starlette.exceptions import HTTPException as StarletteHTTPException
99
from starlette.responses import Response
1010

11+
from sag_py_web_common.response_content import SimpleDetail, ValidationErrorDetail, ValidationErrorResponse
12+
1113
logger: Logger = logging.getLogger("http_error_logger")
1214

1315

@@ -18,10 +20,26 @@ async def handle_unknown_exception(_: Any, exception: Exception) -> JSONResponse
1820
JSONResponse: A json response that contains the field 'detail' with the exception message.
1921
"""
2022
logger.error("An unknown Error!", exc_info=True, extra={"response_status": 500})
21-
return JSONResponse(status_code=500, content=jsonable_encoder({"detail": str(exception)}))
23+
return JSONResponse(status_code=500, content=SimpleDetail(detail=str(exception)).model_dump())
2224

2325

2426
async def log_exception(_, exception: StarletteHTTPException) -> Response: # type: ignore
2527
logger.error("An HTTP Error! %s", exception.detail, extra={"response_status": exception.status_code})
2628

2729
return await http_exception_handler(_, exception)
30+
31+
32+
async def handle_validation_exception(_: Any, exception: RequestValidationError) -> JSONResponse:
33+
errors = [
34+
ValidationErrorDetail(**{
35+
"loc": [str(loc) for loc in err["loc"]],
36+
"msg": err["msg"],
37+
"type": err["type"]
38+
})
39+
for err in exception.errors()
40+
]
41+
logger.error("Validation Error!", exc_info=True, extra={"response_status": 422})
42+
return JSONResponse(
43+
status_code=422,
44+
content=ValidationErrorResponse(detail=errors).model_dump()
45+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from pydantic import BaseModel
2+
from typing import List
3+
4+
5+
class SimpleDetail(BaseModel):
6+
detail: str
7+
8+
9+
class ValidationErrorDetail(BaseModel):
10+
loc: List[str]
11+
msg: str
12+
type: str
13+
14+
15+
class ValidationErrorResponse(BaseModel):
16+
detail: List[ValidationErrorDetail]

tests/test_json_exception_handler.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import logging
2+
from unittest.mock import Mock
23

34
import pytest
45
from _pytest.logging import LogCaptureFixture
6+
from fastapi.exceptions import RequestValidationError
57
from fastapi.responses import JSONResponse
68
from starlette.exceptions import HTTPException as StarletteHTTPException
79

810
from sag_py_web_common import handle_unknown_exception, log_exception
11+
from sag_py_web_common.json_exception_handler import handle_validation_exception
912

1013

1114
@pytest.mark.asyncio
@@ -18,6 +21,22 @@ async def test_handle_unknown_exception() -> None:
1821
assert result.body == b'{"detail":"error message"}'
1922

2023

24+
@pytest.mark.asyncio
25+
async def test_validation_exception_handler() -> None:
26+
# Arrange
27+
exc = RequestValidationError([{"loc": ("body", "title"), "msg": "field required", "type": "value_error.missing"}])
28+
request = Mock()
29+
30+
# Act
31+
result: JSONResponse = await handle_validation_exception(request, exc)
32+
33+
# Assert
34+
assert result.status_code == 422
35+
assert b'"msg":"field required"' in result.body
36+
assert b'"type":"value_error.missing"' in result.body
37+
assert b'"loc":["body","title"]' in result.body
38+
39+
2140
@pytest.mark.asyncio
2241
async def test_log_exception(caplog: LogCaptureFixture) -> None:
2342
# Arrange

0 commit comments

Comments
 (0)