Как проверять кодировку параметров запроса в Python
Что проверяют
Понимаете ли вы, что параметр в URL передаётся не «в кодировке», а как percent-encoded байты, и где именно эти байты превращаются в строку. Следом часто спрашивают, что будет с невалидной последовательностью вроде %FF и почему сервер не упал.
Как отвечать
Разведите две стороны.
Клиент. Строка кодируется в UTF-8, затем небезопасные байты заменяются на %XX. Этим занимаются urllib.parse.quote, quote_plus и urlencode, а requests и httpx делают это сами, если передать params=. Проверить можно без сети: requests.Request("GET", url, params=p).prepare().url покажет итоговый адрес, и «тест» должен превратиться в %D1%82%D0%B5%D1%81%D1%82.
Сервер. request.args во Flask и request.query_params в FastAPI отдают уже декодированные строки, и декодируют их нестрого: невалидные байты заменяются символом � или остаются как %XX, исключения не будет. Поэтому строгая проверка идёт по сырой строке запроса (request.query_string во Flask, request.scope["query_string"] в Starlette):
from urllib.parse import parse_qs
def strict_params(raw: bytes) -> dict[str, list[str]]:
return parse_qs(raw.decode("ascii"), errors="strict")UnicodeDecodeError из этой функции превращаете в 400.
Что ещё проговорить:
+и%20:parse_qsпревращает+в пробел,unquoteнет, аquoteпо умолчанию не кодирует/;- двойное кодирование:
%2520вместо%20означает, что строку закодировали дважды; - тесты: через тестовый клиент Flask или
TestClientFastAPI отправьте кириллицу, эмодзи,&,=,#,%FFи проверьте, что приложение получило исходную строку или ответило400.
Где спотыкаются
Проверяют param.encode("utf-8").decode("utf-8"). Для обычной строки это не падает, и к этому моменту байты уже декодировал фреймворк, так что проверка ничего не проверяет.
Склеивают URL f-строкой: f"{base}?q={text}". Пробел, & или # в значении ломают запрос, нужны urlencode или params=.
Видят в логах � вместо букв и ищут проблему в базе, хотя байты испортил клиент, а фреймворк заменил их при декодировании.