Skip to content

Commit 9d0dea2

Browse files
committed
feat: fix the 422
1 parent c694413 commit 9d0dea2

32 files changed

Lines changed: 419 additions & 139 deletions

docker-compose.yml

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,16 @@ services:
1313
interval: 5s
1414
timeout: 5s
1515
retries: 10
16-
1716
db:
18-
image: postgres:16
17+
image: postgres:17
1918
environment:
2019
POSTGRES_DB: database_app_test
2120
POSTGRES_USER: app
2221
POSTGRES_PASSWORD: secret
2322
ports:
2423
- "5432:5432"
25-
volumes:
26-
- postgres_data:/var/lib/postgresql/data
2724
healthcheck:
28-
test: [ "CMD-SHELL", "pg_isready -U app -d database_app_test" ]
25+
test: [ "CMD", "pg_isready", "-U", "app", "-d", "database_app_test" ]
2926
interval: 5s
3027
timeout: 5s
3128
retries: 10
32-
33-
minio:
34-
image: minio/minio:latest
35-
command: server /data --console-address ":9001"
36-
environment:
37-
MINIO_ROOT_USER: minioadmin
38-
MINIO_ROOT_PASSWORD: minioadmin
39-
ports:
40-
- "9000:9000"
41-
- "9001:9001"
42-
volumes:
43-
- minio_data:/data
44-
healthcheck:
45-
test: [ "CMD", "mc", "ready", "local" ]
46-
interval: 5s
47-
timeout: 5s
48-
retries: 10
49-
50-
volumes:
51-
postgres_data:
52-
minio_data:

example/config-app/uv.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/database-app/app/students/controllers/registration.py

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

3-
from fastapi import HTTPException
3+
from fastapi.exceptions import RequestValidationError
44

55
from app.http.schemas.auth import StudentRegistrationRequest
66
from app.models import User, Profile
@@ -9,7 +9,14 @@
99
async def register(request: StudentRegistrationRequest):
1010
existing_user = await User.where("email", request.email).first()
1111
if existing_user:
12-
raise HTTPException(status_code=400, detail="Email already registered")
12+
raise RequestValidationError(
13+
errors=[{
14+
"loc": ("body", "email"),
15+
"msg": "Email already registered",
16+
"type": "value_error",
17+
"input": request.email,
18+
}]
19+
)
1320

1421
password = hashlib.md5(request.password.encode()).hexdigest()
1522
user = User(

example/database-app/tests/features/students/test_register.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,6 @@ async def test_user_cannot_register_with_duplicate_email(self):
6161
await self.post("/students/register", json=payload)
6262

6363
response = await self.post("/students/register", json=payload)
64-
assert response.status_code == 400
65-
assert response.json()["detail"] == "Email already registered"
64+
assert response.status_code == 422
65+
errors = response.json()["errors"]
66+
assert "email" in errors

example/database-app/uv.lock

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fastapi_startkit/src/fastapi_startkit/fastapi/exceptions.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
class HttpExceptionHandler:
2+
"""
3+
Handles FastAPI HTTPException by returning the exception's own status code and detail.
4+
"""
5+
6+
async def render(self, request, exc):
7+
from fastapi.responses import JSONResponse
8+
9+
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
10+
11+
112
class HTTPExceptionHandler:
213
"""
314
The base exception handler for FastAPI applications.
@@ -26,3 +37,45 @@ async def render(self, request, exc):
2637
content = {"message": "Server Error"}
2738

2839
return JSONResponse(status_code=500, content=content)
40+
41+
42+
class ValidationExceptionHandler:
43+
"""
44+
Handles RequestValidationError with content negotiation.
45+
46+
JSON requests (API clients) receive a 422 Unprocessable Entity response.
47+
Non-JSON requests (browser/Inertia) have errors flashed to the session
48+
and are redirected back to the referring page.
49+
"""
50+
51+
def report(self, exc) -> None:
52+
pass
53+
54+
async def render(self, request, exc):
55+
accept = request.headers.get("accept", "")
56+
content_type = request.headers.get("content-type", "")
57+
58+
wants_json = (
59+
"application/json" in accept
60+
or content_type.startswith("application/json")
61+
)
62+
63+
errors = {}
64+
for err in exc.errors():
65+
field = ".".join(str(x) for x in err["loc"][1:])
66+
errors.setdefault(field, []).append(err["msg"])
67+
68+
if wants_json:
69+
from fastapi.responses import JSONResponse
70+
71+
return JSONResponse(status_code=422, content={"errors": errors})
72+
73+
if "session" in request.scope:
74+
request.session["errors"] = errors
75+
76+
from starlette.responses import RedirectResponse
77+
78+
return RedirectResponse(
79+
url=request.headers.get("referer", "/"),
80+
status_code=303,
81+
)

fastapi_startkit/src/fastapi_startkit/fastapi/providers/fastapi_provider.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi_startkit.fastapi.exceptions import HTTPExceptionHandler
1+
from fastapi_startkit.fastapi.exceptions import HttpExceptionHandler, HTTPExceptionHandler, ValidationExceptionHandler
22
from fastapi import FastAPI
33

44
from fastapi_startkit.fastapi.commands import ServeCommand
@@ -26,6 +26,8 @@ def _register_exception_handlers(self):
2626

2727
exception_manager = self.app.exception_manager
2828
exception_manager.register_handler(Exception, HTTPExceptionHandler())
29+
exception_manager.register_handler(HTTPException, HttpExceptionHandler())
30+
exception_manager.register_handler(RequestValidationError, ValidationExceptionHandler())
2931

3032
async def handler(request, exc):
3133
return await exception_manager.handle(exc, {"request": request})

fastapi_startkit/src/fastapi_startkit/inertia/exceptions.py

Lines changed: 0 additions & 22 deletions
This file was deleted.

fastapi_startkit/src/fastapi_startkit/inertia/provider.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ def register(self) -> None:
1212
self.app.bind("inertia", Inertia)
1313

1414
def boot(self) -> None:
15-
self.register_validation_handler()
16-
1715
self.app.add_middleware(InertiaMiddleware)
1816

1917
if self.app.has("templates"):
@@ -28,8 +26,3 @@ def inertia_helper(page):
2826
templates.env.globals["inertia"] = inertia_helper
2927
templates.env.globals["Inertia"] = self.app.make("inertia")
3028

31-
def register_validation_handler(self) -> None:
32-
from fastapi.exceptions import RequestValidationError
33-
from .exceptions import InertiaValidationHandler
34-
35-
self.app.exception_manager.register_handler(RequestValidationError, InertiaValidationHandler())

fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/connection.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ async def insert(self, query: str, bindings: list | None = None) -> int | None:
107107

108108
return getattr(result, "lastrowid", None)
109109

110+
async def insert_get_id(self, query: str, bindings: list | None = None) -> int | None:
111+
result = await self.execute(query, bindings)
112+
return getattr(result, "lastrowid", None)
113+
110114
async def update(self, query: str, bindings: list | None = None) -> int:
111115
result = await self.execute(query, bindings)
112116

0 commit comments

Comments
 (0)