# теория · шаг 1 из 5
Чем опасен if
Коротко
if внутри location заводит неявный вложенный блок: когда условие
срабатывает, запрос обслуживает он, а не блок вокруг. Отсюда исчезающие
заголовки и proxy_pass, уходящий на другой путь в зависимости от параметра
запроса. Безопасны внутри if ровно четыре директивы: return, rewrite,
set, break. Всё остальное либо не грузится, либо ведёт себя не так, как
написано.
Правило «в if только return и rewrite» знаешь - листай до «Теперь сам». Разборы про исчезнувший заголовок и раздвоившийся прокси.
Блок ставит заголовок безопасности всем ответам, а внутри условия добавляет
второй. Что получит клиент по запросу /hdr?x=1?
location /hdr {
add_header X-Снаружи да always;
if ($arg_x) { add_header X-Внутри да always; }
return 200 "ok\n";
}
Ожидание - два заголовка. На стенде приходит один: X-Внутри. Внешний
исчез.
На что это похоже
Это как отдать заказ на субподряд. Пока условие ложно, работает твой цех со всеми твоими правилами. Как только условие истинно, работу забирает другой цех - формально твой, стоящий в том же здании, но со своим набором правил. Он унаследует то, чего у него нет своего, и не унаследует ничего из того, где у него есть хоть что-то своё. Заголовки как раз из второй категории: правило наследования у них «всё или ничего», и оно уже встречалось в разделе про язык конфига.
Механизм: неявный вложенный блок
Правило
Внутри if можно писать только то, что не относится к обслуживанию запроса:
return, rewrite, set и break. Всё остальное - либо ошибка загрузки,
либо тихая подмена обработчика.
Разбор: заголовок, который исчез
Три замера одного и того же блока:
| запрос | заголовки в ответе |
|---|---|
/hdr?x=1 (условие истинно) |
только X-Внутри |
/hdr (условие ложно) |
только X-Снаружи |
/hdr-bez (блока if нет вовсе) |
только X-Снаружи |
Первая строка и есть дефект. Пока условие ложно, всё выглядит правильно - поэтому такую конфигурацию выкатывают, посмотрев ответ на обычном запросе. Ломается она только на тех запросах, ради которых её и писали.
Если заголовок нужен всем, а второй - по условию, условие уносят из блока в карту:
map $arg_x $вложенный {
default "";
~. "да";
}
server {
location /hdr {
add_header X-Снаружи да always;
add_header X-Внутри $вложенный always; # пустой не отдаётся вовсе
return 200 "ok\n";
}
}
Заголовок с пустым значением nginx не отправляет - значит одна строка заменяет и условие, и оба варианта ответа.
Разбор: один блок, два разных бэкенда
location /api/ {
if ($arg_x) { proxy_pass http://back:8000; }
proxy_pass http://back:8000/внешний/;
}
Конфиг грузится: nginx -t доволен. Замер того, что видит бэкенд:
Бэкенд печатает пришедший путь (return 200 "path = $request_uri"):
curl -s http://localhost/внешний/cart
curl -s 'http://localhost/внешний/cart?x=1'
path = /внешний/cart
path = /api/cart?x=1
Один location отправляет запросы на два разных пути, и решает это параметр
строки запроса. В конфиге об этом не написано ни слова: обе строки выглядят
как один и тот же адрес. Разница в том, что у внешней есть путь /внешний/
(значит, подстановка URI), а у внутренней его нет (значит, URI уходит как
есть).
Для сравнения - try_files внутри if nginx не принимает вовсе:
nginx: [emerg] "try_files" directive is not allowed here in /etc/nginx/nginx.conf:7
То есть часть запрещений проверяется при старте, а часть - нет, и надеяться на
nginx -t как на защиту нельзя.
Что ломается без этого
- Два
ifподряд выполняются оба.set $r "${r}A"иset $r "${r}B"в двух условиях далиr=AB. Это не «первый выигравший», а последовательное выполнение на фазе rewrite - и потому длинные цепочки условий незаметно дорожают. - Проверка
-fсмотрит на$request_filename, то есть уже на собранный путь с учётомrootиalias. В блоке сaliasона проверит не тот файл, о котором идёт речь. ifв блокеserverбезопаснее, чем вlocation, но и там он остаётся фазой rewrite:returnвнутри него сработает раньше выбора location и раньше любых ограничений доступа.
Зачем это в работе
Правило «if is evil» появилось не из вкусовщины: у директивы нет своего
контекста выполнения, она переиспользует механизм вложенного location. Поэтому
на каждую задачу, ради которой тянутся к if, есть замена:
| Задача | Чем заменить |
|---|---|
| условие по имени домена | отдельный блок server со своим server_name |
| условие по началу пути | отдельный location |
| условие по методу запроса | limit_except GET { deny all; } |
| «если файла нет» | try_files $uri $uri/ /index.php; |
| условие по адресу клиента | geo |
| всё остальное | map |
Оставшиеся законные применения if - редирект и перезапись по признаку,
который иначе не выразить: if ($http_user_agent ~* бот) { return 403; }.
Здесь внутри только return, значит неявный блок ничего не ломает.
Теперь сам
Найди, что не так, и скажи, при каком запросе это выстрелит:
location /files/ {
add_header Cache-Control "public, max-age=3600" always;
if ($arg_download) {
add_header Content-Disposition attachment always;
}
alias /srv/files/;
}
Ответ: при ?download=1 из ответа пропадёт Cache-Control - останется только
Content-Disposition. Сработавший if обслуживает запрос сам, а заголовки
наследуются целиком или никак. Лечится картой: map $arg_download $раздача,
и обе строки add_header стоят рядом на одном уровне.
Главное
Сработавший if - это вложенный блок, который забирает обслуживание запроса
себе: внешние add_header пропадают, proxy_pass подменяется. Внутри
безопасны return, rewrite, set, break. Условие по домену - отдельный
server, по пути - отдельный location, по методу - limit_except, всё
прочее - map.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий