1. 1
  2. 2
  3. 3
  4. 4

# теория · шаг 1 из 4

Что наследуется, а что нет

Коротко

Директива из внешнего блока действует и во внутренних, пока её не переопределят. Побеждает ближайшее значение. Исключений два, и оба кусаются.

  • Обычные директивы (root, alias, gzip, expires) наследуются и перекрываются по одной - как ожидаешь.
  • Директивы-списки (add_header, proxy_set_header, limit_req) не дополняются, а замещаются целиком: один такой во вложенном блоке отменяет весь список сверху.
  • Обработчик содержимого не наследуется вовсе. Вложенный location внутри location с proxy_pass НЕ проксирует.

Если правило «ближайший побеждает» и два исключения знакомы - листай до «Теперь сам».

Сначала ответь сам

Есть работающий прокси на приложение. Понадобилось пометить статику заголовком - добавили вложенный блок:

/etc/nginx/conf.d/shop.confhttp server
location /app/ {
    proxy_pass http://127.0.0.1:8080;

    location /app/static/ {
        add_header X-Cached "yes" always;
    }
}

proxy_pass передаёт запрос приложению (подробно - в разделе про проксированиеРазбирается в разделе 7, глава «Слеш в proxy_pass»: Один символ, который меняет путь): 127.0.0.1 - адрес этого же компьютера, 8080 - порт, на котором слушает приложение.

Что вернёт запрос GET /app/static/logo.png?

Инстинктивный ответ - «картинку от приложения, только с новым заголовком». Настоящий - 404. Приложение этого запроса не увидит вообще.

На что это похоже

Наследование в nginx похоже на дресс-код в компании.

Общее правило записано в уставе: «деловой стиль». Отдел может уточнить: «у нас по пятницам свободная форма» - и внутри отдела действует уточнение, а всё остальное берётся из устава. Ровно так работают обычные директивы: пишешь root в server, переопределяешь в одном location, в остальных действует исходный.

А теперь представь правило, устроенное иначе: «список допустимых цветов». Отдел пишет свой список из одного цвета - и общий список не дополняется, а отменяется целиком. Никто не предупреждает; просто с этого дня в отделе разрешён один цвет вместо десяти.

Это и есть директивы-списки, и главная беда с ними в том, что выглядят они как все остальные.

Механизм: три способа собрать значение

При сборке конфига nginx для каждого уровня вычисляет итоговое значение каждой директивы. Способов у него три, и директива выбирает свой сама.

1. обычная директива - берётся ближайшая server: root /a location: root /b → /b 2. директива-список - вложенный список вытесняет внешний server: A, B location: C → только C 3. обработчик содержимого - не наследуется совсем location: proxy_pass вложенный: пусто → отдача с диска Выглядят все три одинаково. Различает их только тип самой директивы.
Три механизма сборки значения. Второй и третий и есть источник почти всех «настройка написана, а не работает».

Обычный случай запоминать не надо - он работает как ждёшь:

/etc/nginx/conf.d/shop.confhttp
server {
    root /var/www/shop;

    location /downloads/ {
        root /mnt/big-disk;      # тут корень другой, ближайший побеждает
    }
}

Запрос /downloads/a.zip nginx ищет в /mnt/big-disk/downloads/a.zip: rootРазбирается в разделе 5, глава «root против alias»: Как URI превращается в путь приклеивает к корню весь путь запроса. Так же ведёт себя alias: замер показал, что location, вложенный в location с alias, берёт файлы по тому же alias.

Правило

Директива наследуется вниз, пока её не переопределят на более внутреннем уровне. Исключений два: список замещается целиком, обработчик содержимого не наследуется.

Разбор: почему вложенный location дал 404

Вернёмся к конфигу из начала урока. Чтобы повторить опыт, приложение не нужно: его роль сыграет сам nginx, ещё двумя server-блоками. Первый печатает адрес, который получил, второй - заголовки X-A и X-B, которые пригодятся ниже:

/etc/nginx/conf.d/backend.confhttp
server {
    listen 8080;
    return 200 "BACKEND $request_uri\n";
}
server {
    listen 8081;
    return 200 "a=$http_x_a b=$http_x_b\n";
}

return 200 "…" отвечает кодом 200 и этим текстом (подробнее о returnРазбирается в разделе 13, глава «return против rewrite»: return: коды, тела, редиректы), $http_x_a - значение заголовка запроса X-A. Проверено на nginx 1.31.5, ответы настоящие.

↳  выводcurl -s localhost/app/x
BACKEND /app/x
↳  выводcurl -so /dev/null -w '%{http_code}' localhost/app/static/logo.png
404

Первый запрос попал в location /app/ и ушёл на бэкенд. Второй попал во вложенный location /app/static/, а там proxy_pass нет: он не наследуется. Обработчика у location не осталось, и nginx сделал то, что делает по умолчанию, - пошёл искать файл на диске. Файла нет - 404.

Обработчик содержимого - директива, которая сама решает, чем ответить: proxy_pass, fastcgi_pass, uwsgi_pass, grpc_pass. Без неё nginx ищет файл на диске.

Если файл на диске случайно есть, всё ещё интереснее: запрос отдаст его, минуя приложение. Именно так утекают исходники и .env, положенные рядом.

Ровно то же самое происходит с try_filesРазбирается в разделе 5, глава «try_files и SPA»: Кандидаты и назначение - директивой «попробуй эти файлы по очереди»: вложенный location её не унаследует, и правило «если файла нет - отдай index.html» перестанет действовать на этой ветке адресов.

Разбор: где наивное правило подводит

Наивное правило: «раз директива написана выше, она действует ниже». Для списков это неверно, и заметить подмену по виду конфига нельзя. Вот проверка на proxy_set_header:

/etc/nginx/conf.d/shop.confhttp
server {
    proxy_set_header X-A "from-server";

    location /only/ { proxy_pass http://127.0.0.1:8081; }
    location /both/ {
        proxy_set_header X-B "from-loc";
        proxy_pass http://127.0.0.1:8081;
    }
}

Бэкенд на порту 8081 печатает то, что получил (&& запускает вторую команду, если первая прошла успешно):

↳  выводcurl -s localhost/only/ && curl -s localhost/both/
a=from-server b=
a= b=from-loc

В /both/ заголовок X-A исчез. Одна добавленная строка отменила все proxy_set_header уровнем выше - в том числе те, которыми бэкенду сообщают настоящий адрес клиента и схему запросаРазбирается в разделе 7, глава «Заголовки: Host, X-Real-IP, X-Forwarded-*»: Что бэкенд перестаёт видеть. Приложение начнёт видеть всех пользователей с адреса прокси, и виновата будет строка, которая про адреса ничего не говорила.

Список директив-списков стоит помнить наизусть, он короткий: add_header, add_trailer, proxy_set_header (и его родня fastcgi_param, uwsgi_param), limit_req, limit_conn, access_log, error_page.

Что ломается без этого

  • 404 или чужой файл там, где раньше был бэкенд. Ищи вложенный location - скорее всего, его добавили ради одной мелочи.
  • Бэкенд перестал видеть настоящий адрес клиента. Ищи proxy_set_header, добавленный в location.
  • Заголовки безопасности пропали на части сайта. Ищи add_header в location - об этом отдельный урок, следующий.
  • Запросы перестали попадать в лог. access_log тоже список: указал его в одном location - потерял назначения, заданные выше.

Зачем это в работе

Оба исключения объединяет одно: их включает добавление строки, а не удаление. Конфиг работал, кто-то дописал одну безобидную директиву - и сломалось что-то соседнее, о чём эта директива не говорила ни слова.

Отсюда практика ревью: правку конфига читают не по диффу, а по вопросу «какой блок она затрагивает целиком». Появился новый location внутри старого - проверь, был ли у родителя обработчик. Появился один add_header или proxy_set_header - проверь, что было в списке выше.

И проверяй опытом, а не памятью: curl -I до правки и после видит разницу быстрее, чем глаз.

Теперь сам

1. В server стоит root /var/www/shop;, а в location /files/ - alias /mnt/storage/;. Что вернёт запрос /files/report.pdf?

Файл /mnt/storage/report.pdf. alias в location перекрывает root сервера, как перекрыл бы его другой root: ближайший побеждает. Отличается сборка пути - alias заменяет совпавшую частьРазбирается в разделе 5, глава «root против alias»: alias: замена вместо приклеивания /files/ целиком, а root приклеил бы весь путь.

2. В server два add_header, в location /api/ один proxy_set_header и proxy_pass. Какие заголовки увидит клиент в ответе на /api/cart?

Оба серверных. proxy_set_header - список заголовков к бэкенду, а add_header - список заголовков клиенту. Это разные списки, и вытесняют они каждый свой. Ловушка срабатывает только между однотипными директивами.

3. Как добавить заголовок к статике, не сломав проксирование из первого примера урока?

Не заводить вложенный location: отобрать статику отдельным location того же уровня, со своим root или со своим proxy_pass. Из /app/ и /app/static/ для /app/static/logo.png nginx выберет второй - у него префикс длиннее (правила выбораРазбирается в разделе 4, глава «Как выбирается location»: Две фазы выбора). Либо, если статику всё же отдаёт приложение, поставить add_header в существующий location /app/ и не создавать вложенности вовсе.

Главное

Директивы наследуются вниз, побеждает ближайшее переопределение - кроме двух случаев: список замещается целиком, а обработчик содержимого не наследуется. Оба включаются добавлением строки, а не удалением, и ломают соседнее.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.