# теория · шаг 1 из 3
Откуда берутся $переменные
Коротко
Переменные начинаются с $ и вычисляются на каждый запрос - не при старте и
не при reload.
- Главная пара:
$uriменяется внутренними преобразованиями,$request_uriнавсегда остаётся тем, что прислал клиент. В редиректах нужен второй. - Вторая пара:
$host- имя, приведённое к нижнему регистру и без порта;$http_host- сырой заголовок как есть. - Любой заголовок запроса доступен как
$http_имя, ответа -$sent_http_имя. - Своя переменная заводится
set(вserver/location) илиmap(вhttp). Просто написать$своёнельзя: конфиг не пройдёт проверку. $1-$9заняты под скобочные группы регулярок - и вне регулярки пусты.
Если разница $uri и $request_uri очевидна - листай до «Теперь сам».
Сначала ответь сам
Типовой редирект на https:
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 - лист, который лежит у тебя на столе. С ним работают: вычёркивают,
переписывают, переадресуют в другой отдел. К моменту, когда до него дойдут
руки, он может выглядеть совсем не так, как конверт.
Ошибка возникает, когда в ответ клиенту переписывают адрес с рабочего листа. Клиент-то помнит конверт.
Механизм: когда переменная получает значение
Переменные ленивы: значение вычисляется в тот момент, когда его спросили. Это важнее, чем кажется, потому что за время обработки запроса состояние меняется.
Проверено, и числа настоящие. Спросишь $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.
Разбор: где наивное правило подводит
Наивное правило: «переменную можно завести где угодно, написав её имя».
add_header X-Note "$my_thing" always;
nginx: [emerg] unknown "my_thing" variable
Обрати внимание: номера строки нет. Nginx обнаруживает это уже после разбора текста, когда искать по файлу приходится тебе. Заводится переменная двумя директивами:
set $my_thing "значение";
Но set работает только в server, location и if - в http его нет:
nginx: [emerg] "set" directive is not allowed here in /etc/nginx/nginx.conf:3
Причина не в прихоти. set выполняется при обработке запроса, а на уровне
http обработки ещё нет. Для уровня http есть map - таблица «из чего во
что», которая тоже считается на запрос:
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 видит прямо сейчас, а не что ты предполагаешь.
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, ничего не потеряв?
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.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий