FastAPI молча игнорирует фильтр: проверяем всю строку query
Клиент запрашивает каталог с параметром limti=5 и получает обычную страницу из двадцати записей. Ответ имеет статус 200, поэтому ошибка долго выглядит как неисправная пагинация. Отдельные аргументы функции проверяют известные поля, но обычно не запрещают остальные параметры запроса. Для строгого поискового интерфейса полезно проверить весь набор сразу.
Сначала выберите договор фильтра
Неизвестный параметр может быть допустимым расширением публичного API. Но в служебном поиске опечатка способна запустить тяжёлую выгрузку вместо маленькой страницы. Здесь договор будет строгим: принимаются limit и status, предел страницы — сто записей, статус выбирается из двух значений. Остальные имена приводят к 422. Клиент сразу узнаёт, какое поле не распознано.
Для этого модель подключается именно через Query. Модель обычного тела запроса не увидит строку URL, а модель, созданная вручную из пары известных аргументов, уже потеряет неизвестные имена. Поддержка моделей query-параметров появилась в FastAPI 0.115.0; для старого проекта сначала проверьте установленную версию. Ограничения Pydantic остаются внутри модели и попадают в OpenAPI.
Проверим опечатку и настоящий поиск
Сохраните блок как Python-файл и запустите с установленными fastapi и httpx, Pydantic v2. TestClient выполняет запросы к приложению без отдельного сервера. Маленький каталог нужен, чтобы проверить влияние фильтра на результат, а не только успешное создание модели.
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field
class Filters(BaseModel):
model_config = ConfigDict(extra="forbid")
limit: int = Field(20, ge=1, le=100)
status: Literal["open", "closed"] = "open"
app = FastAPI()
rows = [{"id": 1, "status": "open"}, {"id": 2, "status": "closed"}]
@app.get("/tickets")
def tickets(filters: Annotated[Filters, Query()]):
return [r for r in rows if r["status"] == filters.status][:filters.limit]
with TestClient(app) as client:
typo = client.get("/tickets?limti=1")
assert typo.status_code == 422
assert typo.json()["detail"][0]["loc"] == ["query", "limti"]
assert client.get("/tickets?limit=101").status_code == 422
assert client.get("/tickets?status=archived").status_code == 422
result = client.get("/tickets?status=closed&limit=1")
assert result.status_code == 200
assert result.json() == [{"id": 2, "status": "closed"}]
print({"typo": typo.status_code, "result": result.json()})
Почему это помогает диагностике
Путь ошибки начинается с query и заканчивается ошибочным именем. Frontend может подсветить именно фильтр, а журнал запросов покажет воспроизводимый URL. При неверном статусе ошибка относится к status, при превышении страницы — к limit. Это разные причины отказа, хотя HTTP-статус у них одинаковый. Не заменяйте их одним сообщением «неверный запрос», если интерфейс умеет показать точную причину.
Когда добавляется новый фильтр, изменяйте модель и запрос к хранилищу вместе. Поле, объявленное в схеме, но не использованное при выборе данных, создаёт особенно неприятную ошибку: валидация проходит, документация выглядит правильно, результат не меняется. Поэтому в тесте есть закрытая заявка, которую невозможно получить с исходным фильтром open.
В большом приложении проверьте ещё общие query-параметры инфраструктуры. Например, клиентская библиотека может добавлять cache-buster. Строгий договор потребует согласовать его или перенести на другой уровень. После этого сохраните один тест с посторонним именем: он обнаружит случайную замену модели набором аргументов маршрута. Для JSON-тела похожее ограничение разбирается в статье о лишних полях; способ подключения здесь другой.
Как вводить строгий фильтр в существующем API
Перед включением запрета посмотрите реальные запросы клиентов и выпишите используемые имена. Если часть клиентов передаёт старый параметр, сначала согласуйте его удаление или объявите временную совместимость. Иначе исправление опечаток одновременно станет изменением публичного договора. Для внутреннего инструмента обычно достаточно обновить вызывающий код и включить отрицательный тест в тот же релиз.
Сортировку тоже лучше объявлять закрытым набором значений и сопоставлять заранее подготовленным выражениям базы. Проверенная строка sort не должна безусловно становиться частью SQL через форматирование. Модель определяет разрешённый ввод, а слой запросов выбирает безопасное выражение по этому значению. Такой порядок делает новый фильтр проверяемым от URL до результата поиска.
Продолжить на практике
Следующая задача — собрать фильтрацию, получение записей из базы и HTTP-тесты в одном API. Бесплатный курс связывает параметры маршрутов с CRUD и SQLAlchemy: на этой основе удобно закрепить строгий договор поиска. Откройте бесплатную программу FastAPI.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
