Крайние случаи URL-кодирования: пробел, плюс и процент
Рано или поздно каждый разработчик выпускает фичу, которая таинственным образом ломается, как только кто-то вставляет в URL значение с пробелами, плюсами или не-ASCII символами. Корень проблемы почти всегда один и тот же: тонкие различия в том, как URL-кодирование работает в браузере, на бэкенде и в промежуточных прокси.
В этой статье мы разберём реальные сценарии отладки вокруг percent encoding, + против %20, а также различия между RFC 3986 и application/x-www-form-urlencoded. Мы также посмотрим, как использовать URL encoder/decoder, чтобы проверять query-строки ещё до того, как они попадут на бэкенд.
Два мира: RFC 3986 против application/x-www-form-urlencoded
Когда разработчики говорят «URL-кодирование», они часто смешивают два немного разных набора правил:
- RFC 3986 URL encoding — описывает, как символы percent-кодируются в URI и URL.
- application/x-www-form-urlencoded — описывает, как браузеры кодируют поля HTML-форм в query-строку или тело запроса.
Хитрость в том, что обе схемы используют percent encoding, но формат form-encoded по‑особому обращается с пробелом.
| Контекст | Символ пробела | Пример |
|---|---|---|
| RFC 3986 (общий URL) | %20 | q=hello%20world |
| application/x-www-form-urlencoded | + (плюс) | q=hello+world |
В form-encoded данных + означает пробел, а не буквальный плюс. В обычном RFC 3986 URL + — просто ещё один символ, и если вам нужен именно плюс, его нужно percent-кодировать как %2B.
История отладки: «C++» превращается в «C »
Представьте, что вы отлаживаете поиск по языкам программирования. Пользователь жалуется, что поиск по C++ возвращает результаты для C.
Фронтенд, на первый взгляд, шлёт вполне нормальный запрос:
GET /search?query=C++ HTTP/1.1
Host: example.testНо если браузер или клиент не кодирует плюсы корректно, реальная строка запроса может оказаться такой:
GET /search?query=C%2B%2B # Правильно (плюсы закодированы)
GET /search?query=C++ # Двусмысленно (может быть интерпретировано как "C ")На сервере, если фреймворк трактует query как application/x-www-form-urlencoded, он декодирует + в пробел. Результат — "C " вместо "C++".
Безопасное правило: кодируйте буквальный плюс как %2B. Если сомневаетесь, пропустите значение через URL encoder и проверьте результирующую строку.
Резервированные, нерезервированные и специальные символы
RFC 3986 делит символы на три крупные группы:
- Нерезервированные: безопасны без кодирования — буквы, цифры,
-,.,_,~ - Резервированные: имеют структурное значение в URL —
:,/,?,#,[,],@,!,$,&,',(,),*,+,,,;,= - Всё остальное: должно быть percent-кодировано (включая пробелы, не-ASCII, управляющие символы)
В query-строке резервированные символы часто используются как разделители:
&разделяет пары «ключ-значение»=разделяет имя и значение?начинает query-часть
Если вам нужны эти символы внутри значения, их нужно кодировать. Например, параметр, содержащий JSON, должен кодировать ", { и }, а также любые &, чтобы не сломать структуру URL.
Как работает percent encoding
Percent encoding заменяет байт в URL на %, за которым следуют две шестнадцатеричные цифры в верхнем регистре, представляющие значение этого байта.
" " (пробел) -> %20
"+" (плюс) -> %2B
"%" (процент) -> %25
"?" (вопрос) -> %3F
"&" (амперсанд)-> %26Для не-ASCII символов в UTF-8 каждый байт кодированной последовательности кодируется отдельно:
"привет" (UTF-8 байты):
пр -> D0 BF
ив -> D0 B8 D0 B2
ет -> D0 B5 D1 82
Закодированное URL-значение:
%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82URL encoder/decoder особенно полезен в таких случаях: вставьте Unicode-строку, проверьте percent-кодированный вывод и убедитесь, что ваш бэкенд декодирует её обратно без порчи байтов.
Query string encoding на практике
Допустим, вы хотите отправить несколько фильтров в одном параметре query-строки в виде JSON. Сам JSON выглядит так:
{
"status": ["open", "closed"],
"assignee": "alice@example.com"
}Если наивно вставить его в URL без кодирования, получится некорректная query-строка:
/issues?filter={"status":["open","closed"],"assignee":"alice@example.com"}Символы вроде ", {, }, а также @ и & конфликтуют с правилами парсинга URL.
Безопасный вариант — percent-кодировать JSON-значение:
/issues?filter=%7B%22status%22%3A%5B%22open%22%2C%22closed%22%5D%2C%22assignee%22%3A%22alice%40example.com%22%7DЗдесь помогает понятный workflow для query-строк: собираете JSON, пропускаете его через URL encoder и только потом добавляете в URL.
Типичные ошибки URL-кодирования
Ошибка 1: двойное кодирование
Двойное кодирование возникает, когда значение percent-кодируется больше одного раза. Например, пробел превращается в %20, а затем символ % сам кодируется в %25, и получается %2520.
// Первое кодирование:
"hello world" -> "hello%20world"
// Повторное кодирование:
"hello%20world" -> "hello%2520world"На сервере один проход декодирования даёт hello%20world, а не hello world. В итоге вы сохраняете буквально %20 в базу или сравниваете неправильную строку при поиске.
Для отладки декодируйте значение один раз через URL decoder. Если вы всё ещё видите percent-последовательности, скорее всего, строка была закодирована несколько раз.
Ошибка 2: отсутствие кодирования значений в query-строке
Конкатенация строк при построении URL — классический источник ошибок:
// Хрупко: без кодирования
const url = "/search?query=" + query;
// Надёжнее: кодируем значение
const url = "/search?query=" + encodeURIComponent(query);Если забыть закодировать значения, символы вроде & и = ломают структуру query-строки и «обрезают» параметры. Всегда пропускайте сырое вводимое пользователем значение через URL encoder, прежде чем вставлять его в query.
Ошибка 3: кодирование всего URL вместо отдельных значений
Другая распространённая ошибка — вызывать URL encoder на всей строке URL, включая разделители и слэши:
// Неправильно: кодируются "/", "?", "&" и "="
encodeURIComponent("/search?query=hello world");
// Правильно: кодируем только динамическое значение
"/search?query=" + encodeURIComponent("hello world");Если перекодировать всю строку, сервер больше не видит нормальный URL с путём и query-частью. Многие фреймворки будут трактовать его как путь с буквальными %3F и %3D, и вы потеряете все query-параметры.
Использование URL-инструмента в стиле DevToys в рабочем процессе
Отдельный URL encoder/decoder крайне полезен при отладке web-пейлоадов. С DevToys Pro URL Encoder/Decoder вы можете:
- Вставить сырую query-строку и сразу увидеть декодированные ключи и значения
- Кодировать произвольный текст (включая пробелы, плюсы и Unicode) в безопасное значение для URL
- Проверять, не была ли строка закодирована дважды, декодируя её шаг за шагом
- Сравнивать, как разные входные данные меняют закодированный вывод в крайних случаях
- Экспериментировать с query string encoding до того, как добавить его в production-код
Лучшие практики URL-кодирования
1. Кодируйте значения, а не весь URL
Применяйте URL-кодирование только к динамическим частям URL (query-параметры, сегменты пути), а не ко всей строке. Так разделители ?, &, = и / останутся нетронутыми, и парсер URL сможет корректно отработать.
2. Осторожно обращайтесь с «+»
- В form-encoded данных
+означает пробел. - Чтобы передать буквальный
+в значении, кодируйте его как%2B. - Если вы видите пробелы вместо плюсов, проверьте, как сервер декодирует
application/x-www-form-urlencoded.
3. Используйте полноценный encoder
Не пишите самодельные замены вида replace(" ", "%20"). Используйте полноценный URL encoder в своём языке и сверяйтесь с онлайн URL encoder/decoder, чтобы убедиться, что браузер, сервер и инструменты одинаково понимают семантику.
4. Логируйте и сырые, и декодированные значения
При отладке проблем в продакшене логируйте как сырую строку запроса, так и уже декодированные параметры, которые видит фреймворк. Разница между ними часто выдаёт двойное кодирование, отсутствие кодирования или неправильную обработку спецсимволов.
Выводы
URL-кодирование кажется простым, пока в игру не вступают пробелы, плюсы и не-ASCII символы. Понимание различий между RFC 3986 и application/x-www-form-urlencoded, аккуратная работа с + и единообразное percent-кодирование специальных символов позволяют избежать тонких багов в query-строках.
Сочетание корректного query string encoding в коде и надёжного URL encoder/decoder в вашем наборе инструментов даёт уверенность, что то, что пользователь видит в адресной строке, — ровно то же самое, что ваш бэкенд получает и декодирует.