Ошибка 422 вернула секрет: сокращаем ответ валидации FastAPI
Форма подключения интеграции принимает имя сервиса и секрет. Пользователь случайно передаёт секрет объектом вместо строки. В штатном ответе валидации может оказаться исходное значение input. Такое отражение удобно при разработке, но клиентская телеметрия нередко сохраняет весь JSON ошибки. Одна неудачная форма отправляет лишние данные в ещё одну систему.
Задайте разрешённые поля ответа
Обработчик RequestValidationError позволяет сформировать внешний договор независимо от внутреннего представления ошибки. Для интерфейса достаточно пути поля и машинного типа ошибки. Мы вернём массив issues с location и code, сохранив 422. Не будем включать input, исходное тело и строковое представление исключения. Список разрешённых полей проще проверять при обновлении зависимостей, чем удаление нескольких известных опасных ключей.
Здесь речь о возвращаемом HTTP-документе. Отдельно найдите место, где ваш middleware или error tracker записывает запросы: сокращённый ответ сам по себе не меняет эти журналы. В обработчике также не нужно бездумно вызывать logger.exception с объектом ошибки. Если расследованию нужны подробности, решите, какие поля допустимы для записи и кому доступен журнал.
Проверка должна содержать узнаваемый маркер
Понадобятся fastapi, httpx и Pydantic v2. В тесте используется выдуманный секрет-маркер. Успешный endpoint возвращает только имя интеграции. Так один блок проверяет и отказ валидации, и нормальную форму ответа.
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Integration(BaseModel):
name: str
secret: str
@app.exception_handler(RequestValidationError)
async def validation_error(request: Request, exc: RequestValidationError):
issues = [{"location": list(e["loc"]), "code": e["type"]}
for e in exc.errors()]
return JSONResponse(status_code=422, content={"issues": issues})
@app.post("/integrations")
def create(data: Integration):
return {"name": data.name}
with TestClient(app) as client:
bad = client.post("/integrations", json={
"name": "billing", "secret": {"value": "marker-secret-42"}})
assert bad.status_code == 422
assert bad.json() == {"issues": [
{"location": ["body", "secret"], "code": "string_type"}]}
assert "marker-secret-42" not in bad.text
ok = client.post("/integrations", json={
"name": "billing", "secret": "marker-secret-42"})
assert ok.status_code == 200 and ok.json() == {"name": "billing"}
print({"invalid": bad.json(), "created": ok.json()})
Сохраните удобство исправления формы
location остаётся массивом, потому что вложенная ошибка может содержать строковые имена и числовой индекс. Превращение пути в строку через точку создаёт неоднозначность, если сами ключи содержат точки. Frontend получает устойчивую структуру и может сопоставить её полю. code помогает локализовать объяснение без зависимости от английского сообщения библиотеки.
Если пользовательские валидаторы создают свои типы ошибок, внесите их в клиентский словарь. Для неизвестного кода оставьте общее предложение проверить поле. Не возвращайте ctx целиком ради удобного форматирования: его содержимое зависит от валидатора и может включать дополнительные объекты. При необходимости добавляйте конкретные разрешённые ограничения, например верхнюю границу длины.
После изменения формата обновите описание 422 в OpenAPI. Регистрация exception_handler меняет выполнение, но генератор схемы не угадывает ваш новый конверт. Клиенты, построенные по старой схеме, должны получить согласованное изменение. В реальном проекте добавьте также вложенную модель и отсутствие обязательного поля: это подтвердит обработку путей разной глубины.
Не перехватывайте этим обработчиком ResponseValidationError. Ошибка сформированного сервером ответа означает дефект приложения, а не ввод пользователя. Для ожидаемых конфликтов предметной области подходит отдельный обработчик доменной ошибки. Разделение сохраняет полезное различие между исправлением формы и исправлением серверного кода.
Как проверить изменение без настоящих секретов
Сделайте отдельный маркер для каждого чувствительного поля и отправьте неверный тип, слишком длинное значение и вложенный объект. После каждого запроса проверьте весь текст ответа на отсутствие маркера. Проверка только одного ключа input пропустит значение, которое случайно попало в message или дополнительный контекст. При этом отдельно проверьте сохранность location, чтобы исправление не лишило форму диагностики.
Для тестового error tracker используйте такую же запись маркера и прочитайте собранное событие. Это уже проверка конкретной интеграции журналирования, её нужно выполнять там, где она подключена. Сам обработчик ответа не видит дальнейшую обработку исключения сторонним middleware, поэтому две проверки закрывают разные места распространения исходного ввода.
Продолжить на практике
На следующем шаге проверьте ошибки CRUD через настоящий маршрут и подключите собственные модели. В бесплатной программе FastAPI есть HTTP-ошибки и автотесты; используйте их, чтобы закрепить внешний договор формы. Откройте бесплатную программу FastAPI.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
