From c0e162df720b25929c76219c5c9cbb2e2dbad90c Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Sun, 13 Sep 2026 15:02:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D0=B8:=20422=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D1=8D=D1=85=D0=B0=20=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=B2?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Если JSON-эндпойнт получал тело с Content-Type text/plain (так шлёт HTML-форма), сервер отвечал 500. _validation_handler клал exc.errors() в details как есть, а в input там лежало сырое тело типа bytes, которое JSONResponse не сериализует. Затронуты все эндпойнты с телом, в том числе /api/auth/telegram и /api/admin/auth/login. Запрос и раньше отклонялся, сессия не создавалась, но вместо понятной 422 в логе появлялся трейсбек. Тот же input возвращал клиенту тело запроса целиком. Неполная форма входа отправляла пароль обратно в ответе 422, откуда он мог осесть в логах прокси и инструментах отладки. Теперь в details идут только type, loc и msg. input и ctx (там бывают объекты исключений — та же проблема сериализации) в ответ не попадают. Конверт и код VALIDATION_ERROR прежние, фронт details валидации не читает. Тесты: text/plain на двух эндпойнтах даёт 422, пароль не возвращается, loc по-прежнему указывает на поле. На старом коде первые два падают. #52 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6 --- backend/app/main.py | 6 ++++- backend/tests/test_validation_errors.py | 31 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_validation_errors.py diff --git a/backend/app/main.py b/backend/app/main.py index ad9fca6..d58e61b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -177,13 +177,17 @@ def create_app() -> FastAPI: @app.exception_handler(RequestValidationError) 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( status_code=422, content={ "error": { "code": "VALIDATION_ERROR", "message": "Ошибка валидации запроса.", - "details": exc.errors(), + "details": details, } }, ) diff --git a/backend/tests/test_validation_errors.py b/backend/tests/test_validation_errors.py new file mode 100644 index 0000000..d7fc215 --- /dev/null +++ b/backend/tests/test_validation_errors.py @@ -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 -- 2.54.0