FastAPI получает только один тег: принимаем повторяющиеся query-параметры
В каталоге добавили фильтр по нескольким тегам. Браузер отправляет /products?tag=python&tag=api, а обработчик видит только одно значение. При ручной проверке с единственным тегом всё работало, поэтому ошибка обнаружилась уже после подключения интерфейса. Причина обычно в объявлении параметра: одна строка описывает одно значение, хотя клиент передаёт последовательность.
Сначала зафиксируйте договор с клиентом. В этой статье каждый тег передаётся отдельной парой с одинаковым ключом. Запятая внутри значения остаётся обычным символом. Отсутствие ключа означает отсутствие фильтра, а пустой тег считается ошибкой ввода. Такой договор удобно проверить отдельно от запроса к базе: сперва убедимся, какие именно значения дошли до обработчика.
Объявляем список именно в query
Для списка строк нужен явный Query: он сообщает FastAPI, где искать данные. Запись через Annotated объединяет тип списка и описание источника. Внутренний тип каждого элемента дополнительно ограничен через StringConstraints: строка после удаления крайних пробелов должна содержать хотя бы один символ. Пустое значение не должно неожиданно превратиться в поиск по пустому тегу.
Значение None по умолчанию позволяет полностью опустить параметр. Внутри обработчика такой случай превращается в пустой список. Дубли убираются с сохранением первого появления: если интерфейс случайно передаст тег дважды, запрос к хранилищу останется понятным. В другой предметной области повторы могут иметь смысл; тогда это преобразование следует убрать и явно описать их обработку.
from typing import Annotated
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import StringConstraints
Tag = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
app = FastAPI()
@app.get("/products")
def products(tag: Annotated[list[Tag] | None, Query()] = None):
return {"tags": list(dict.fromkeys(tag or []))}
with TestClient(app) as client:
r = client.get("/products", params=[("tag", "python"), ("tag", "api"), ("tag", "python")])
assert r.status_code == 200
assert r.json() == {"tags": ["python", "api"]}
assert client.get("/products").json() == {"tags": []}
for value in ["", " "]:
assert client.get("/products", params={"tag": value}).status_code == 422
assert client.get("/products", params={"tag": "python,api"}).json() == {"tags": ["python,api"]}
print("received tags:", r.json()["tags"])
Запрос в первом тесте передаёт параметры списком пар. Обычный словарь с двумя одинаковыми ключами потерял бы повтор ещё до отправки: второй ключ заменил бы первый. На сервере получаются два тега, и порядок соответствует первому появлению. Следующие проверки отделяют три ситуации: параметр отсутствует, передана пустая строка, передана одна строка с запятой. Они часто выглядят одинаково в интерфейсе, хотя означают разные входные данные.
Где искать потерю значения
Откройте в браузере сведения о сетевом запросе и посмотрите полный URL. Если там уже один tag, исправлять нужно сборку запроса в клиенте. Если повторения присутствуют, проверьте сигнатуру маршрута и промежуточный код, который превращает query в обычный словарь. Для чтения из объекта запроса у многозначных параметров нужен способ получить весь список, а не только одно скалярное значение.
При передаче из JavaScript используйте добавление каждой пары в URLSearchParams. На стороне Python HTTP-клиента подходит список пар, как в тесте. Самостоятельно склеивать URL неудобно: тег может содержать пробел, плюс или амперсанд. Пусть клиент кодирует значения. Связанный разбор кодирования query в Go показывает, как такие символы меняют смысл адреса.
После получения правильного списка ещё предстоит решить, что означает несколько тегов для каталога. Требуются товары хотя бы с одним тегом или сразу со всеми? Способ передачи параметров этого не определяет. Зафиксируйте правило в запросе к базе и добавьте небольшой набор товаров, на котором эти варианты дают разные результаты. Тогда тест транспорта не будет ошибочно считаться проверкой поиска.
Для слишком длинного URL разумно ограничить число тегов и длину каждого, исходя из реального каталога и ограничений инфраструктуры. Не переносите такой фильтр в JSON-тело только ради случайной ошибки объявления: сначала проверьте форму списка в описании Query и сгенерированной схеме /docs. Контракт должен одинаково читаться клиентом, сервером и тестами.
При переносе существующего метода сначала проверьте, как им пользуются другие клиенты. Если кто-то уже присылает строку с запятыми, новое объявление списка сохранит её целиком. Молча поддерживать оба формата рискованно: настоящий тег тоже может содержать запятую. Лучше согласовать один способ передачи, обновить примеры запросов и оставить совместимость только с явно определённым правилом преобразования старого формата.
От параметра к целому API
В бесплатном курсе FastAPI можно продолжить путь: валидация входа, CRUD, SQLite через SQLAlchemy и HTTP-тесты. Это следующий уровень примера — применить список тегов к данным и проверить ответ API.
Если в проекте уже много собственного Python-кода, платный курс «Python для профессионалов» — посмотрите программу развивает другую часть подготовки: итераторы, генераторы, декораторы, менеджеры контекста и асинхронность. Вы сможете разбирать преобразования коллекций и управление ресурсами глубже, чем на уровне одного параметра маршрута. Практику именно с FastAPI продолжайте по отдельной ссылке выше.
Продолжить на практике
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
