1. 1
  2. 2
  3. 3

# теория · шаг 1 из 3

Откуда берутся $переменные

Коротко

Переменные начинаются с $ и вычисляются на каждый запрос - не при старте и не при reload.

  • Главная пара: $uri меняется внутренними преобразованиями, $request_uri навсегда остаётся тем, что прислал клиент. В редиректах нужен второй.
  • Вторая пара: $host - имя, приведённое к нижнему регистру и без порта; $http_host - сырой заголовок как есть.
  • Любой заголовок запроса доступен как $http_имя, ответа - $sent_http_имя.
  • Своя переменная заводится setserver/location) или maphttp). Просто написать $своё нельзя: конфиг не пройдёт проверку.
  • $1-$9 заняты под скобочные группы регулярок - и вне регулярки пусты.

Если разница $uri и $request_uri очевидна - листай до «Теперь сам».

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

Типовой редирект на https:

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

returnРазбирается в разделе 13, глава «return против rewrite»: return: коды, тела, редиректы отвечает сразу, не ища файлов: здесь кодом 301 («переехало навсегда») и новым адресом.

Пользователь пришёл по ссылке из письма: http://shop.local/catalog/42?utm_source=mail.

Куда его отправит этот конфиг?

Инстинктивный ответ - на ту же страницу, только по https. Настоящий - на https://shop.local/catalog/42, без строки запроса. Метка рекламной кампании потеряна, и вместе с ней - вся статистика по этой рассылке.

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

Две переменные - как оригинал письма и рабочая копия.

$request_uri - конверт, который принёс курьер. Что на нём написано, то и написано; в архиве он лежит неизменным.

$uri - лист, который лежит у тебя на столе. С ним работают: вычёркивают, переписывают, переадресуют в другой отдел. К моменту, когда до него дойдут руки, он может выглядеть совсем не так, как конверт.

Ошибка возникает, когда в ответ клиенту переписывают адрес с рабочего листа. Клиент-то помнит конверт.

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

Переменные ленивы: значение вычисляется в тот момент, когда его спросили. Это важнее, чем кажется, потому что за время обработки запроса состояние меняется.

запрос принят rewrite / try_files отдача ответа запись в лог $request_uri не меняется никогда $uri после перезаписи - другое значение $body_bytes_sent осмыслен только тут
Одна и та же переменная в разных местах конфига может дать разное значение. Спрашивают её в момент использования.

Проверено, и числа настоящие. Спросишь $status на фазе rewriteРазбирается в разделе 13, глава «return против rewrite»: Фазы: кто и когда решает - раннем шаге обработки, где работают set, if и return, то есть до того, как ответ сформирован, - получишь 000. Спросишь его же в add_header - получишь верный код: заголовки дописываются, когда код уже решён. А $body_bytes_sent в том же add_header даст 0, хотя access_log запишет 43 байта - столько весила тестовая страница: тело ещё не отправлено, считать нечего.

Правило

$request_uri - то, что прислал клиент, целиком и навсегда. $uri - текущий путь внутри nginx, без строки запроса, и он меняется после rewrite и try_files. Всё, что уезжает клиенту в ответ, строится из $request_uri.

Разбор: десять переменных на каждый день

Их несколько сотен, ежедневно нужны примерно десять. Значения настоящие, снято на nginx 1.31.5 для запроса GET /catalog/42?sort=price с заголовком Host: Shop.Local:80.

Переменная Значение Когда нужна
$uri /catalog/42 путь после внутренних преобразований
$request_uri /catalog/42?sort=price исходный URI как прислал клиент
$args sort=price строка запроса целиком
$arg_sort price один параметр по имени
$is_args ? пусто, если параметров нет: удобно склеивать
$host shop.local имя, в нижнем регистре и без порта
$http_host Shop.Local:80 сырой заголовок Host как прислали
$remote_addr 172.17.0.1 адрес того, кто подключился; здесь шлюз Docker, через который пришёл curl
$scheme http схема запроса
$request_id 67fcaf5f… уникальный id запроса из 32 знаков, генерирует сам nginx

Повторить у себя: поставь отладочный заголовок из раздела «Зачем это в работе» и отправь запрос с чужим Host - curl -sI -H 'Host: Shop.Local:80' 'localhost/catalog/42?sort=price'.

Пара $host и $http_host разъезжается чаще, чем ожидаешь. $host - причёсанный: приведён к нижнему регистру, порт отрезан, а если заголовка HostЗаголовок HostПо нему nginx выбирает server-блок, когда на одном адресе живёт несколько сайтов. Бэкенду по умолчанию уходит не он, а имя группы серверов - отсюда письма со ссылками на внутренние адреса. нет вовсе, подставится имя из server_name. $http_host - буквально то, что пришло. Для редиректов и proxy_set_header Host берут $host: он всегда осмысленный.

И общее правило вместо запоминания: любой заголовок запроса доступен как $http_имя, где имя в нижнем регистре, а дефисы заменены подчёркиваниями. User-Agent$http_user_agent, X-Forwarded-For$http_x_forwarded_for. Симметрично $sent_http_ даёт заголовки ответа: Content-Type$sent_http_content_type.

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

Наивное правило: «переменную можно завести где угодно, написав её имя».

/etc/nginx/conf.d/shop.confhttp server
add_header X-Note "$my_thing" always;
↳  выводnginx -t
nginx: [emerg] unknown "my_thing" variable

Обрати внимание: номера строки нет. Nginx обнаруживает это уже после разбора текста, когда искать по файлу приходится тебе. Заводится переменная двумя директивами:

/etc/nginx/conf.d/shop.confhttp server location
set $my_thing "значение";

Но set работает только в server, location и if - в http его нет:

↳  выводnginx -t
nginx: [emerg] "set" directive is not allowed here in /etc/nginx/nginx.conf:3

Причина не в прихоти. set выполняется при обработке запроса, а на уровне http обработки ещё нет. Для уровня http есть map - таблица «из чего во что», которая тоже считается на запрос:

/etc/nginx/nginx.confhttp
map $http_user_agent $is_bot {
    default   0;
    "~*bot"   1;
}

Слева в строках - с чем сравнивать значение первой переменной ($http_user_agent), справа - что положить во вторую ($is_bot). "~*bot" - регулярка без учёта регистра, «в строке есть bot»; default - значение, если не подошла ни одна строка.

После этого $is_bot доступен в любом server-блоке. Проверено: обычный браузер даёт 0, curl -A Googlebot/2.1 даёт 1 (-A подменяет заголовок User-Agent). Увидеть значение можно тем же отладочным заголовком: add_header X-Bot $is_bot always;.

Второе, обо что спотыкаются: $1-$9 тебе не принадлежат. Это скобочные группы последней сработавшей регулярки, и вне регулярки они пусты. Строка "цена $5 рублей" отдаст клиенту цена рублей, а обратный слэш перед долларом не спасает - об этом был урок про синтаксис.

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

  • После редиректа пропали utm-метки и параметры поиска. В редиректе стоит $uri вместо $request_uri.
  • Приложение видит всех пользователей с одного адреса. Потерян proxy_set_header X-Forwarded-For - либо его вытеснил список уровнем ниже.
  • unknown "…" variable без номера строки. Ищи опечатку в имени переменной: $reqest_uri, $remote_add. Помогает nginx -T | grep -n '\$имя': обратный слэш здесь для grep, а не для nginx - в шаблоне grep $ значит «конец строки», и слэш делает его обычным знаком; -n печатает номер строки.
  • Переменная пустая или нулевая, хотя объявлена. Проверь, не $1 ли это и не спрашиваешь ли ты значение раньше, чем оно появилось: $status на фазе rewrite равен 000, а $body_bytes_sent в заголовке ответа - нулю.

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

Переменные - главный инструмент отладки на живом сервере, потому что они показывают, что nginx видит прямо сейчас, а не что ты предполагаешь.

/etc/nginx/conf.d/shop.confhttp server
add_header X-Debug "uri=$uri req=$request_uri host=$host" always;

Одна строка, curl -I, и все догадки заменяются фактом: применился ли rewrite, какой Host дошёл, что осталось от адреса. Тот же приём во втором варианте - дописать переменные в log_format, если проблема воспроизводится редко и ловить её надо на потоке (свой формат логаРазбирается в разделе 6, глава «access_log и log_format»: Свой формат: время, бэкенд, request_id).

Убирать отладочный заголовок перед выкладкой обязательно: $uri и $host безобидны, а вот $http_cookie или $http_authorization, попавшие в заголовок ответа, - это утечка, и она сохранится в логах промежуточных прокси.

Теперь сам

1. Как написать редирект на https, ничего не потеряв?

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

$request_uri несёт и путь, и строку запроса. Склейка $uri$is_args$args выглядит так же, но после rewrite в $uri уже новый путь, и он раскодирован. Замер: запрос /rw/a%20b?utm=1, переписанный в /vars2/…, даёт склейку /vars2/a b?utm=1 - другой путь и голый пробел, - а $request_uri остаётся /rw/a%20b?utm=1.

2. В логе видно, что пользователи приходят на Shop.Local, а сравнение if ($host = "shop.local") в конфиге срабатывает. Противоречие?

Нет. В лог обычно пишут $http_host или $host - смотря что в log_format. $host уже приведён к нижнему регистру, поэтому сравнение работает. Если в логе видны заглавные буквы, значит, там $http_host.

3. Нужно пометить запросы от мобильных приложений отдельным заголовком. Куда положить сопоставление и почему не set?

В map на уровне http. Правило одно на весь сервер, и держать его в одном месте правильнее, чем повторять set в каждом location. Плюс map вычисляется лениво: если переменную на этом запросе никто не спросил, таблицу даже не посмотрят.

Главное

Переменные считаются на каждый запрос, в момент обращения. Всё, что уезжает клиенту, строится из $request_uri - $uri к тому времени мог измениться. Свои переменные заводит set в server/location и map на уровне http.

Комментарии

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

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

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