1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 2 из 7

Адрес по частям: что доезжает до сервера

Коротко

Адрес в браузерной строке и то, что получает сервер, - разные вещи. Часть адреса уезжает в заголовок, часть не уезжает вовсе, а часть приезжает в двух видах сразу.

  • В строке запроса едут только путь и параметрыПараметры запросаПары «ключ=значение», разделённые амперсандом. В nginx лежат в переменной $args и в выборе location не участвуют вовсе - location смотрит только на путь.. Схема и хост - не там: хост приходит заголовком Host.
  • Всё после # до сервера не доезжает никогда.
  • Путь приезжает в двух переменных: $request_uri - как прислали, $uri - раскодированный и нормализованный. location выбирается по второму.
  • Параметры ($args) nginx не раскодирует вообще: %20 там так и остаётся %20.
  • Слишком длинный адрес - это 414 и строка в логе на уровне info.

Если разница $uri и $request_uri и судьба фрагмента знакомы - листай до «Теперь сам».

Сначала ответь сам

Человек открыл в браузере:

↳  выводадресная строка
https://shop.local/catalog/шкафы?sort=price#отзывы

Какие части этого адреса увидит nginx в строке запроса?

Не схему и не хост - в строке запроса их нет вовсе, а имя сайта приедет отдельным заголовком Host. И не фрагментФрагментУказание браузеру, к какому месту страницы прокрутиться. На сервер он не отправляется никогда, поэтому в конфиге nginx его нельзя ни увидеть, ни поймать правилом.: всё после # браузер оставляет себе, сервер о нём не узнает никогда. Останется путь и параметры, причём кириллица будет закодирована:

↳  вывод$request с настоящего стенда
GET /catalog/%D1%88%D0%BA%D0%B0%D1%84%D1%8B?sort=price HTTP/1.1

Отсюда сразу следует практическое: правило в конфиге, написанное «по адресу из браузера», может не сработать - сервер видит другую строку.

На что это похоже

Почтовый адрес на конверте состоит из частей, и у каждой своя судьба.

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

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

Механизм: что из адреса куда попадает

https://shop.local/catalog/42?sort=price#otzyvy схема решает порт и шифрование, в запрос не едет хост едет отдельным заголовком Host путь первая строка запроса; по нему выбирается location параметры там же после «?»; в выборе location не участвуют фрагмент остаётся в браузере, на сервер не уходит $request_uri = /catalog/42?sort=price $uri = /catalog/42 $args = sort=price
Три нижние строки - три разные переменные над одним и тем же адресом. Путать их - самая частая ошибка в редиректах.

Правило

$request_uri - что прислали, $uri - что получилось после раскодирования и нормализации. Решения о маршруте nginx принимает по второму, а в редиректах почти всегда нужен первый.

Разбор: одна строка в трёх переменных

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

$  команда
curl --path-as-is 'http://lab.local/vars/a%2Fb%20c?x=a+b%20c'
↳  выводчто показали переменные
uri=/vars/a/b c
request_uri=/vars/a%2Fb%20c?x=a+b%20c
args=x=a+b%20c

Три вещи разом:

  • %20 в пути стал пробелом, а %2F - слешем. То есть в $uri появился разделитель каталогов, которого в исходном адресе не было;
  • $request_uri сохранил всё как прислали, вместе с ? и параметрами;
  • $args не раскодирован вообще: там как был a+b%20c, так и остался. Плюс тоже остался плюсом, хотя в параметрах он по традиции значит пробел - раскодировать его должно приложение.

Кириллица ведёт себя так же: /vars/файл.txt в $uri, а в $request_uri - двадцать четыре процента с цифрами.

Разбор: где наивное правило подводит

Фрагмент нельзя поймать в конфиге. Ни location, ни if, ни map его не увидят - его нет в запросе. Просьба «сделай редирект для ссылок с #promo» невыполнима на стороне сервера, и это не ограничение nginx, а устройство протокола.

Тонкость: если кривой клиент всё-таки отправит # в строке запроса, nginx положит его в $request_uri, но отрежет от $uri - проверено. То есть даже в этом случае маршрутизация фрагмент не увидит.

%2F в пути превращается в обычный слеш ещё до выбора location. Правило курса из раздела про locationURIURIТо, что идёт после метода: путь плюс необязательные параметры после вопросительного знака. Имени сайта в нём нет - оно приезжает заголовком Host, - а фрагмента после решётки нет тем более. нормализуется до выбора») работает и здесь, и следствие у него двойное. Хорошее: запрет deny не обойти, закодировав слеш. Плохое: путь /files/a%2Fb.txt, в котором слеш был частью имени файла, превратится в путь на два уровня, и файл не найдётся.

Редирект по $uri теряет параметры. Строка return 301 https://$host$uri; выглядит правильной, но выбрасывает всё после ?: человек пришёл по ссылке с utm-метками, а уехал на голый адрес. Нужен $request_uri, и он уже включает ?:

/etc/nginx/conf.d/shop.local.confhttp server
return 301 https://$host$request_uri;

Что ломается без этого

Три типовые беды растут из одной путаницы. Редирект, который теряет параметры поиска, - это $uri вместо $request_uri. Аналитика, где половина переходов без меток, - тот же редирект. Правило, которое «не срабатывает», хотя адрес совпадает буква в букву, - сравнение с закодированной строкой при том, что nginx сравнивает с раскодированной.

Зачем это в работе

Длина адреса не бесконечна. Заголовки и строка запроса читаются в буфер, и по умолчанию его хватает примерно на восемь килобайт. Адрес длиннее даёт 414:

$  команда
curl -s -o /dev/null -w 'код=%{http_code}\n' "localhost/$(head -c 9000 /dev/zero | tr '\0' a)"
↳  выводcurl -s -o /dev/null -w 'код=%{http_code}\n' "localhost/$(head -c 9000 /dev/zero | tr '\0' a)"
код=414

Выражение в $( ) собирает адрес из девяти тысяч букв a, а -w печатает код ответа.

↳  выводстрока в error_log, уровень info
[info] client sent too long URI while reading client request line

У строки метка info - это уровень логаРазбирается в разделе 6, глава «access_log и log_format»: Что пишется без тебя, одна из восьми ступеней серьёзности; настройка error_log отсекает всё, что ниже названной. При типовом уровне - error в пакете Debian или warn из инструкций - этой строки не будет, и 414 останется без объяснения. Лечится large_client_header_buffers, но сначала стоит выяснить, кто и зачем шлёт такие адреса: обычно это самодельный клиент, складывающий список идентификаторов в параметры вместо тела запросаТело сообщенияВсё, что идёт после пустой строки: содержимое файла, JSON, загружаемая картинка. У обычного GET его нет, а у ответов 204 и 304 не бывает по определению..

Теперь сам

В конфиге стоит редирект return 301 https://$host$uri;. Отдел маркетинга жалуется: переходы с рекламы приходят в аналитику без источника, хотя ссылки размечены метками. Что случилось и как чинить?

Метки живут в параметрах, а $uri их не содержит - редирект отправляет человека на адрес без ?utm_source=..., и аналитика видит прямой заход. Заменить на $request_uri, который несёт и путь, и параметры целиком. Проверить просто: curl -sI 'http://shop.local/page?a=1' и посмотреть, что стоит в Location.

Главное

В строку запроса едут только путь и параметры; хост приезжает заголовком Host, а фрагмент после # не уходит на сервер никогда и в конфиге недоступен. $request_uri - строка как прислали, $uri - раскодированная и нормализованная, по ней выбирается location; $args не раскодируется вовсе. %2F в пути становится настоящим слешем ещё до маршрутизации. В редиректах почти всегда нужен $request_uri - иначе теряются параметры. Адрес длиннее буфера даёт 414, а объяснение пишется на уровне info.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.