Я не собирался писать на эту тему. Разговор неизбежно скатится к набившему оскомину 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, и какая-нибудь обобщенная страничка или скромная сводная табличка с (не)наследуемыми директивами в документации могли бы снять возникающие вокруг этой темы вопросы.