7 мин чтенияИнженерия

URL-кодирование объяснено: какие символы нужно экранировать

URL-кодирование заменяет символ знаком процента и двумя шестнадцатеричными цифрами, чтобы его нельзя было прочитать как синтаксис URL. Какие символы нужно кодировать и где это ломается.

Marius Voß
DevRel · edge infra
URL-кодирование показано как строка запроса, где пробел и амперсанд становятся процентными последовательностями внутри значения одного параметра

URL-кодирование заменяет символ знаком процента и двумя шестнадцатеричными цифрами: пробел становится %20, амперсанд становится %26, вопросительный знак становится %3F. Смысл в том, чтобы символ не прочитали как синтаксис URL, когда вы имели в виду данные. Ничего больше.

Кажется, что это сложнее, потому что почти любой вопрос об этом на самом деле - вопрос про область действия. Какие символы, в какой части URL, экранированные каким слоем? Ошибётесь с областью действия - и получите один из двух классических провалов: трекинговый параметр, который незаметно обрезается, либо адрес назначения, который приходит как https%3A%2F%2Fexample.com и отдаёт 404. Эта статья разбирает два набора символов, которые определяют ответ, места, где правила меняются, и то, как проверить, что на самом деле несёт ссылка. Более широкую картину того, что редирект делает со всем этим, смотрите в статье типы редиректов.

Два набора, которые решают всё

Раздел 2.3 RFC 3986 определяет незарезервированный набор, который никогда не нужно кодировать: буквы, цифры и ровно четыре знака пунктуации - дефис, точка, подчёркивание, тильда. Если в вашем значении есть только они, делать ничего не нужно.

Всё остальное попадает в одну из двух корзин. Зарезервированные символы несут структурное значение: : / ? # [ ] @ разделяют части URL, а ! $ & ' ( ) * + , ; = разделяют вещи внутри этих частей. Раздел 2.2 перечисляет их. Как синтаксис они легальны, и их нужно кодировать, когда они выступают в роли данных. Всё остальное - это символы за пределами ASCII, которые кодируются побайтово после преобразования в UTF-8 - именно поэтому одна буква с диакритикой обычно стоит шесть символов, а не три.

Отсюда единственное правило, которое стоит запомнить: кодируйте символ, когда он является данными и иначе будет прочитан как синтаксис. Амперсанд между двумя параметрами - это синтаксис. Амперсанд внутри названия кампании - это данные, и если оставить его как есть, список параметров на этом обрежется.

Строка запроса, где значение кампании содержит пробел и амперсанд, показанная закодированной правильно внутри значения и неправильно по всему URL

Кодируйте значение, а не URL

Это самая частая ошибка, которую я вижу, и она всегда одной и той же формы. У кого-то есть URL, он знает, что его нужно закодировать, поэтому вставляет всё целиком в кодировщик и получает:

https%3A%2F%2Fexample.com%2Fspring%3Futm_campaign%3Dspring%20sale

Эта строка - не URL. Это текст в форме URL, который может быть только значением внутри другого URL - именно там ему и место, когда вы передаёте адрес назначения через редиректор, и именно там ему не место, когда вы пытаетесь его открыть.

Правильный подход - кодировать каждое значение отдельно:

https://example.com/spring?utm_campaign=spring%20sale&utm_source=flyer

Схема, хост, разделители пути и символы ? и & остаются синтаксисом. Изменилось только значение. В каждом языке для этого различия есть две функции, и выбрать не ту - вторая половина проблемы: страница MDN про encodeURIComponent прямо говорит, что encodeURI намеренно не трогает зарезервированные символы, потому что ожидает целый URI, тогда как encodeURIComponent экранирует их, потому что ожидает лишь его фрагмент. Для значений нужен encodeURIComponent. В Python это urllib.parse.quote, в Go - url.QueryEscape, в PHP - rawurlencode.

Пробел - это %20, кроме тех случаев, когда это плюс

Оба варианта верны, но в разных местах, и это самая сбивающая с толку вещь во всей теме.

В пути или в обычном URI пробел - это %20. В строке запроса, построенной так, как её строит HTML-форма, пробел - это +, потому что именно это определяет сериализация application/x-www-form-urlencoded в стандарте WHATWG URL. Обе формы будут прочитаны как пробел любым парсером запросов на стороне сервера, который вы скорее всего встретите.

Ловушка - в обратном направлении. Если знак плюс - это данные - номер телефона, поисковый запрос, кампания с названием spring+summer - его нужно писать как %2B. Оставленный как есть в строке запроса, он превращается в пробел, и вы потратите полдня, гадая, почему номер в вашей CRM потерял код страны.

СимволКодПочему это важно
пробел%20 или ++ только внутри строки запроса, %20 - везде
&%26Без кодирования список параметров обрывается на этом месте
?%3FБез кодирования всё, что после него, становится запросом
#%23Без кодирования остаток вообще не доходит до сервера
+%2BБез кодирования в запросе он приходит как пробел
%%25Без кодирования съедаются следующие два символа

Строка # заслуживает отдельного комментария, потому что именно она порождает самый непонятный баг-репорт. Фрагмент никогда не отправляется на сервер. Поставьте незакодированный # в цели редиректа - и сервер увидит обрезанный URL, тогда как адресная строка браузера будет выглядеть нормально, так что человек, сообщающий о проблеме, будет клясться, что ссылка в порядке.

Если вы строите URL кампаний вручную чаще, чем иногда, остановитесь: наш конструктор UTM кодирует каждое значение по ходу набора, а статья правила именования UTM разбирает, как выбирать значения, которым кодирование вообще не потребуется. Сократите результат на собственном домене - и закодированную кашу больше никому не придётся разглядывать.

Двойное кодирование и как его заметить

Двойное кодирование происходит, когда значение проходит через два слоя, каждый из которых честно делает свою работу. Сам знак процента - это символ, который нужно экранировать, так что %20 превращается в %2520, а %2520 превращается в %252520.

Симптомы легко узнать, если вы их уже видели. Заголовок страницы, который показывает реальному посетителю spring%20sale. Параметр, который приходит в аналитику с видимыми escape-последовательностями. Редирект, который срабатывает на первом переходе и ломается на втором. Причина почти всегда одна: вызов кодирования обёрнут вокруг значения, которое пришло уже закодированным, часто потому что оно вышло из базы данных, хранившей закодированную форму.

Решение - определить, какой слой отвечает за кодирование, и оставить остальные в стороне. Декодируйте один раз, когда читаете значение, кодируйте один раз, когда записываете его в URL, и никогда не делайте оба действия в одной и той же функции.

Значение проходит через два слоя кодирования, так что пробел становится %20, а затем %2520, с видимым симптомом в браузере

Где это кусает на практике

Три места, в том порядке, в котором вы скорее всего с ними встретитесь.

Трекинговые параметры. Значение кампании с незакодированным амперсандом обрезает список параметров, так что сессия попадает в вашу аналитику как прямой трафик, и кампания не получает никакого зачёта. Никакой ошибки при этом не возникает. Статья UTM-параметры не отображаются в GA4 разбирает диагностику со стороны отчётности, а браузеры вырезают UTM-параметры разбирает другую причину, по которой параметр может пропасть между кликом и страницей.

Редиректы. Серверные правила перекодируют непоследовательно, и переживёт ли строка запроса редирект вообще, зависит от того, какую директиву вы использовали. Статья 301-редирект в .htaccess содержит полную таблицу для Apache; короткая версия - правило, которое заменяет строку запроса, тихо отбросит вашу.

QR-коды. Кодирование увеличивает длину полезной нагрузки, а длина полезной нагрузки определяет, насколько плотным получится напечатанный код. Каждый пробел стоит три символа вместо одного, каждая буква с диакритикой - шесть. Трекинговый URL с парой закодированных названий кампаний может поднять код на версию-другую, а это реальная разница на размере визитки - именно поэтому статья QR-код не сканируется включает длину полезной нагрузки в число четырёх причин. Кодировать короткую ссылку вместо полного URL - самое дешёвое доступное решение.

Проверьте, что на самом деле несёт ссылка

Две команды решают почти любой спор. Первая показывает, что получает сервер после редиректа:

curl -sI 'https://example.com/spring?utm_campaign=spring%20sale' | grep -i '^location'

Вторая строит кодирование сама, а не доверяет вашим пальцам, что удобно, когда значение содержит сразу несколько проблемных символов:

curl -G --data-urlencode 'utm_campaign=spring & summer sale' \
  --data-urlencode 'utm_source=flyer' \
  -o /dev/null -w '%{url_effective}\n' https://example.com/spring

Читайте вывод как данные, а не как украшение. Если вы видите %2520 - у вас проблема двойного кодирования, если значение обрывается раньше времени - у вас незакодированный разделитель, а если в начале видите %3A%2F%2F - вы закодировали весь URL. Наш чекер ссылок делает часть с редиректом прямо в браузере, если вам не хочется открывать терминал.

Привычка, которую стоит выработать, - один раз взглянуть на итоговый URL глазами, прежде чем кампания уйдёт в эфир. Ошибки кодирования невидимы в браузере и очевидны в терминале, и они стоят вам атрибуции, а не аптайма - именно поэтому они живут так долго.

Читайте цикл опорных статей

Эта статья входит в инженерный кластер. Про сторону редиректов статья типы редиректов разбирает каждый код статуса и клиентский метод, а как работают сокращатели URL разбирает, что происходит между кликом и страницей.

Связанные статьи в блоге

Частые вопросы

Что такое URL-кодирование?

Замена символа знаком процента с последующим его байтовым значением в шестнадцатеричном виде, чтобы символ нельзя было спутать с синтаксисом URL. Пробел становится %20, амперсанд становится %26, вопросительный знак становится %3F. Механизм определён в RFC 3986 и также называется процентным кодированием.

Какие символы нужно кодировать в URL?

Всё, что выходит за пределы незарезервированного набора, который RFC 3986 определяет как буквы, цифры и четыре символа - дефис, точку, подчёркивание и тильду. Всё остальное - это либо зарезервированная пунктуация, несущая структурное значение, либо байт за пределами ASCII, и в обоих случаях символы нужно процентно кодировать, когда они появляются внутри значения, а не как синтаксис.

Кодировать весь URL или только его части?

Только части. Если пропустить весь URL через кодировщик, https://example.com превращается в https%3A%2F%2Fexample.com, а это уже вовсе не URL. Кодируйте значение каждого параметра запроса и каждый сегмент пути отдельно, а схему, хост и разделители не трогайте.

Пробел - это %20 или знак плюс?

И то, и другое, в разных местах. В пути и в общем URI пробел - это %20. В строке запроса, построенной так, как её строят HTML-формы, пробел - это знак плюс, потому что именно это определяет сериализация application/x-www-form-urlencoded. Поэтому буквальный плюс внутри значения запроса нужно писать как %2B, иначе его прочитают как пробел.

Что такое двойное кодирование?

Кодирование того, что уже было закодировано, так что %20 превращается в %2520, потому что сам знак процента экранируется до %25. Симптом - страница, которая показывает буквальный %20 в тексте, или параметр, который приходит с видимыми escape-последовательностями. Почти всегда причина - значение, прошедшее через два слоя, каждый из которых заботливо закодировал его ещё раз.

Почему закодированные символы усложняют сканирование QR-кода?

Потому что каждый из них обходится тремя символами вместо одного. Пробел - это один символ смысла и три символа полезной нагрузки, так что горстка таких символов может поднять код на версию-другую, а значит - больше модулей на той же площади печати. Закодировать длинный трекинговый URL в QR - один из самых быстрых способов получить код, который сканируется только с близкого расстояния.

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Бесплатно, без регистрации · 2 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
url encoding
percent encoding
encodeuricomponent
query string
utm parameters
url shortener

Читать дальше