Pull to refresh

Nginx. О чем не хотелось писать

Reading time5 min
Views12K

Я не собирался писать на эту тему. Разговор неизбежно скатится к набившему оскомину IfisEvil. На самом деле это измусоленный вопрос и мне кажется, что вся проблема и шумиха вокруг него заключается лишь в том, что в документации нет последовательного ответа на корень этой проблемы. Поэтому сейчас поговорим про... наследование.

Наследование директив в nginx - это классная штука. Именно наследование позволяет писать простые и понятные конфиги. При слиянии конфигураций значение директивы и её функциональность переходит из вышестоящего контекста в текущий. Логично, что наследование не происходит от параллельных контекстов, например от соседнего location или if.

Вроде бы всё хорошо. Пока не возникают исключения.

N.B.: Здесь и далее описывается работа с nginx версии 1.21.1 (если не указано иное). Всё сказанное основывается лишь на опыте и ошибках автора. Вместе с тем автор не является разработчиком nginx и даже его маститым сварщиком, поэтому не стоит принимать слова автора как догму, а, наоборот, подвергать сомнению и самостоятельному тестированию.

На практике оказывается, что не все директивы могут наследоваться. Так в location не наследуются:

  1. директивы модуля rewrite:

    • break

    • if

    • return

    • rewrite

    • set

  2. try_files

  3. директивы модулей фазы 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 императивны, поэтому:

  1. установится a = 0;

  2. далее a = 0:1, так как условие первого if истинно;

  3. условие второго if также истинно, поэтому a = 0:1:2;

  4. закончив с if'ами возвращаемся в location и сейчас a = 0:1:2:3

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

Tags:
Hubs:
+20
Comments9

Articles