Запрос с телом не в JSON отвечает 500 вместо 422 (#52) #54

Merged
NotBigGhost merged 1 commits from issue-52-validation-500 into dev 2026-09-13 15:26:10 +03:00
2 changed files with 36 additions and 1 deletions
Showing only changes of commit c0e162df72 - Show all commits
+5 -1
View File
@@ -177,13 +177,17 @@ def create_app() -> FastAPI:
@app.exception_handler(RequestValidationError) @app.exception_handler(RequestValidationError)
async def _validation_handler(_request: Request, exc: RequestValidationError) -> JSONResponse: async def _validation_handler(_request: Request, exc: RequestValidationError) -> JSONResponse:
# Только type/loc/msg. В input лежит тело запроса: эхо паролей в ответ, а для тела
# не в JSON (text/plain от HTML-формы) — сырые bytes, которые JSON не сериализует,
# и ответ падал в 500. В ctx бывают объекты исключений — та же проблема.
details = [{"type": e["type"], "loc": e["loc"], "msg": e["msg"]} for e in exc.errors()]
return JSONResponse( return JSONResponse(
status_code=422, status_code=422,
content={ content={
"error": { "error": {
"code": "VALIDATION_ERROR", "code": "VALIDATION_ERROR",
"message": "Ошибка валидации запроса.", "message": "Ошибка валидации запроса.",
"details": exc.errors(), "details": details,
} }
}, },
) )
+31
View File
@@ -0,0 +1,31 @@
"""Ошибки валидации запроса: всегда 422 в едином конверте и без эха тела запроса."""
from __future__ import annotations
from fastapi.testclient import TestClient
def test_non_json_body_is_422_not_500(client: TestClient):
"""HTML-форма шлёт text/plain: тело приходит сырыми bytes, ответ раньше падал в 500."""
for path, raw in [
("/api/auth/telegram", '{"id": 1}'),
("/api/admin/auth/login", '{"username": "a", "password": "b"}'),
]:
r = client.post(path, content=raw, headers={"Content-Type": "text/plain"})
assert r.status_code == 422, (path, r.text)
assert r.json()["error"]["code"] == "VALIDATION_ERROR"
def test_validation_error_does_not_echo_body(client: TestClient):
secret = "very-secret-password"
r = client.post("/api/admin/auth/login", json={"password": secret})
assert r.status_code == 422, r.text
assert secret not in r.text
details = r.json()["error"]["details"]
assert details and all(set(d) == {"type", "loc", "msg"} for d in details)
def test_validation_error_points_to_field(client: TestClient):
r = client.post("/api/admin/auth/login", json={"password": "x"})
locs = [d["loc"] for d in r.json()["error"]["details"]]
assert ["body", "username"] in locs