Я не собирался писать на эту тему. Разговор неизбежно скатится к набившему оскомину IfisEvil. На самом деле это измусоленный вопрос и мне кажется, что вся проблема и шумиха вокруг него заключается лишь в том, что в документации нет последовательного ответа на корень этой проблемы. Поэтому сейчас поговорим про... наследование.
Наследование директив в nginx - это классная штука. Именно наследование позволяет писать простые и понятные конфиги. При слиянии конфигураций значение директивы и её функциональность переходит из вышестоящего контекста в текущий. Логично, что наследование не происходит от параллельных контекстов, например от соседнего location или if.
Вроде бы всё хорошо. Пока не возникают исключения.
N.B.: Здесь и далее описывается работа с nginx версии 1.21.1 (если не указано иное). Всё сказанное основывается лишь на опыте и ошибках автора. Вместе с тем автор не является разработчиком nginx и даже его маститым сварщиком, поэтому не стоит принимать слова автора как догму, а, наоборот, подвергать сомнению и самостоятельному тестированию.
На практике оказывается, что не все директивы могут наследоваться. Так в location не наследуются:
директивы модуля rewrite:
break
if
return
rewrite
set
try_files
директивы модулей фазы CONTENT выполняющих некую явную обработку запросов (за исключением отдачи статики):
grpc_pass
fastcgi_pass
memcached_pass
proxy_pass
scgi_pass
uwsgi_pass
empty_gif
flv
mp4
stub_status
perl
Кроме того часть директив наследуется с предыдущего уровня конфигурации при условии, что на данном уровне не описаны свои директивы (эта информация есть в документации, но указана для каждой директивы отдельно):
limit_req
sub_filter
ssl_conf_command
scgi_param
add_header
add_trailer
uwsgi_param
uwsgi_ssl_conf_command
fastcgi_param
xslt_param и xslt_string_param
proxy_set_header
proxy_ssl_conf_command
grpc_set_header
grpc_ssl_conf_command
error_page
limit_conn
Эти исключения иногда заставляют дублировать директивы, в частности, необходимо писать proxy_pass в каждый location и т.п., но к ним привыкаешь и воспринимаешь это как фичу, пока не возникает if. If не признает исключений, он наследует всё, при этом не всё работает так как ожидается, и в разных версиях nginx может давать разные результаты. Например, ранее (где-то до версии 1.8.0) запрос к example.com/if-and-alias/1.html
location ~* ^/if-and-alias/(?<file>.*) { alias /tmp/$file; set $true 1; if ($true) { # nothing } }
приводил к ошибке 404 Not Found даже при наличии запрашиваемого файла в /tmp. Для текущей версии такой проблемы не возникает, но это не значит, что можно начинать безоглядно использовать if в location. Многие проблемы остались, точнее, так и задумано.
Not really bug, just how it works.
Пример и цитата приведенные выше взяты из документа. Разберем из него первую же задачку, но добавим ещё одну директиву add_header в контекст server:
add_header X-Zero 0; location /only-one-if { set $true 1; if ($true) { add_header X-First 1; } if ($true) { add_header X-Second 2; } return 204; }
В этой конфигурации будет установлен только заголовок X-Second, остальные директивы add_header будут проигнорированы:
$ HEAD -S "example.com/only-one-if" | grep -F 'X-' X-Second: 2
В общем-то всё логично. Каждый if создаёт своего рода вложенный location, в который наследуются директивы с предыдущего уровня, но (как писалось выше) директивa add_header не наследуется с предыдущего уровня конфигурации, так как на данном уровне описаны свои директивы. Разумеется, директива не должна наследоваться и от соседнего if, поэтому заголовок будет установлен только в том if, который (при его истинности) обработается последним. Но исполнится директива add_header только по окончании фазы REWRITE, то есть, при наличии директив модуля ngx_http_rewrite_module, они все исполнятся до обработки add_header:
location /only-one-if { set $true 1; set $a 0; if ($true) { set $a $a:1; add_header X-First $a; } if ($true) { set $a $a:2; add_header X-Second $a; } set $a $a:3; return 204; }
$ HEAD -S "example.com/only-one-if" | grep -F 'X-' X-Second: 0:1:2:3
Директивы ngx_http_rewrite_module императивны, поэтому:
установится a = 0;
далее a = 0:1, так как условие первого if истинно;
условие второго if также истинно, поэтому a = 0:1:2;
закончив с if'ами возвра��аемся в location и сейчас a = 0:1:2:3
и только теперь, завершив return'ом фазу REWRITE, в фильтре устанавливается заголовок X-Second = 0:1:2:3
Как это выглядит в отладчике
$ cat /tmp/error.log | grep -iE "(if|var|set|header|value)" | cut -d ' ' -f 6- ... using configuration "/only-one-if" http script value: "1" http script set $true http script value: "0" http script set $a http script var http script var: "1" http script if http script complex value http script var: "0" http script set $a http script var http script var: "1" http script if http script complex value http script var: "0:1" http script set $a http script complex value http script var: "0:1:2" http script set $a http set discard body http script var: "0:1:2:3" ADD HEADER: 'X-Second' => '0:1:2:3' http finalize request: 0, "/only-one-if?"
Аналогичное поведение в задаче взятой отсюда:
location /proxy { proxy_pass http://127.0.0.1:8080/$a; set $a 32; if ($a = 32) { set $a 56; } set $a 76; } location ~ /(\d+) { echo $1; }
Снова последовательно исполняются все директивы set: a = 32, a = 56, a = 76, только после этого переходим к следующим фазам и, выполняя проксирование на себя же, получаем в ответ:
$ GET -S "example.com/proxy" GET http://example.com/proxy 200 OK 76
После предыдущего разбора этот пример уже не кажется непонятным, но здесь заодно разберем разницу между вложенным location и if:
location /proxy { proxy_pass http://127.0.0.1:8080/$a; set $a 32; if ($a = 32) { set $a 56; } set $a 76; location /proxy/location { } }
И эта конфигурация дает нам
$ GET -Sd "example.com/proxy/location" GET http://example.com/proxy/location 404 Not Found
так как директива proxy_pass не наследуется в location.
А вот этот жуткий пример
location /proxy { proxy_pass http://127.0.0.1:8080/$a; set $a 32; if ($a = 32) { set $a 56; } set $a 76; if ($uri ~ '/proxy/location') { } }
ввиду того, что if пытается наследовать всё, отрабатывает успешно:
$ GET -S "example.com/proxy/location" GET http://example.com/proxy/location 200 OK 76
Разумеется, делать так не надо!
По сути, из нашего "ненаследуемого" списка, остался только try_files:
location /itf { try_files /file @fallback; set $true 1; if ($true) { # nothing } } location @fallback { echo "fallback"; }
Здесь мы не попадем в "@fallback", if просто игнорирует существование try_files, более того, внутри if не удастся указать данную директиву. Использование вложенного location:
location /itf { try_files /file @fallback; set $true 1; location /itf/itf2 { } }
также не даст результата:
$ GET -Sd "example.com/itf/itf2" GET http://example.com/itf/itf2 404 Not Found
так как и в location директива try_files не наследуется. Но здесь уже можно указать её дополнительно.
Вот собственно и всё - никаких багов в if нет. Есть особенности реализации, где-то общие, где-то свои для if и свои для location, и какая-нибудь обобщенная страничка или скромная сводная табличка с (не)наследуемыми директивами в документации могли бы снять возникающие вокруг этой темы вопросы.
