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

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

Кандидаты и назначение

Коротко

Почти все странности отдачи статики растут из одного места: последний аргумент try_files устроен не так, как все остальные.

  • Все аргументы, кроме последнего, - кандидаты. Проверяются на диске по правилам root/alias, отдаётся первый найденный.
  • Последний - назначение. На существование не проверяется вообще. Форм три: =код, @имя, URI.
  • Назначение-URI запускает обработку заново: новый выбор location, новый круг директив.
  • Если новый URI снова попадает в тот же try_files - цикл и 500. Попадёт в другой location - обычный 404. Одна и та же опечатка, два разных симптома.
  • Каждый кандидат стоит обращения к диску, поэтому список держат коротким.

Если различие «кандидат / назначение» уже очевидно - листай до «Теперь сам».

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

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

location / {
    try_files $uri $uri/ =404;
}

На диске есть about.html и каталог docs/ с index.html внутри. Три запроса: /about.html, /docs/, /report.

Первые два очевидны. Вопрос про третий: почему nginx вернул 404, а не стал искать файл с именем 404?

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

try_files - это связка ключей и запасной план.

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

Ошибка почти всех - считать записку последним ключом. Отсюда и вопросы вида «почему nginx не ищет файл 404»: потому что это не кандидат, это план на случай неудачи.

Механизм: кандидаты и одно назначение

try_files $uri $uri/ =404; кандидаты путь строится по root/alias проверяется наличие на диске первый найденный отдаётся назначение на диске НЕ проверяется =код | @имя | URI URI запускает обработку заново Один аргумент - ошибка конфигурации: директива без назначения бессмысленна, и это единственное, что поймает nginx -t.
Две неравные части. Отсюда и ответ на вопрос из начала урока: =404 стоит последним, значит это назначение, а не имя файла.

Три формы назначения:

try_files $uri $uri/ =404;          # вернуть код
try_files $uri $uri/ /index.html;   # внутреннее перенаправление на URI
try_files $uri @backend;            # передать в именованный location

Кандидат со слешем на конце ($uri/) - это проверка каталога, а не файла. Каталог существует - запрос обрабатывается как обычный запрос на каталог: включается index, отдаётся index.html изнутри. Порядок кандидатов и есть порядок приоритета.

Правило

Все аргументы, кроме последнего, проверяются на диске; последний - назначение и не проверяется. Назначение-URI начинает обработку заново, с нового выбора location.

Разбор: одна опечатка, два разных симптома

Внутреннее перенаправление - это новый круг: заново выбирается location, заново применяются директивы. Метод при этом остаётся прежним: POST, который try_files отправил на статический файл, получает 405 - проверено. Наружу ничего не уходит, браузер ничего не замечает.

Отсюда классическая авария - и вот тут интересное. Проверено на nginx 1.31.5, целевого файла на диске нет ни в одном случае.

Это два разных сервера, у обоих root /www. Сервер А:

/etc/nginx/conf.d/shop.confhttp server
# А: цель попадает В ТОТ ЖЕ location
location / { try_files $uri /index.html; }
$  команда
curl -so /dev/null -w '%{http_code}\n' localhost/report
↳  выводcurl -so /dev/null -w '%{http_code}\n' localhost/report
500

-o /dev/null выбрасывает тело ответа (/dev/null - «файл», который глотает всё), -w печатает код. Запрос не нашёл файла, ушёл на /index.html, попал в тот же location /, снова не нашёл, снова ушёл... nginx считает круги, на десятом сдаётся и отвечает 500. Предел зашит в сам nginx, настройкой он не меняется:

↳  выводtail -n 1 /var/log/nginx/error.log
2026/09/10 19:49:50 [error] 20#20: *9 rewrite or internal redirection cycle while internally redirecting to "/index.html", client: 172.17.0.1, server: , request: "GET /report HTTP/1.1", host: "localhost"

Сервер Б, где location / нет вовсе:

/etc/nginx/conf.d/shop.confhttp server
# Б: цель попадает в ДРУГОЙ location
location /app/ { try_files $uri /netu.html; }
$  команда
curl -so /dev/null -w '%{http_code}\n' localhost/app/report
↳  выводcurl -so /dev/null -w '%{http_code}\n' localhost/app/report
404

Цель ушла за пределы /app/, там try_files нет, и nginx честно поискал файл по общим правилам сервера:

↳  выводtail -n 1 /var/log/nginx/error.log
2026/09/10 19:53:56 [error] 9#9: *2 open() "/www/netu.html" failed (2: No such file or directory), client: 127.0.0.1, server: , request: "GET /app/report HTTP/1.1", host: "localhost"

Если бы в сервере Б был ещё и location / из сервера А, цель попала бы в него - и снова цикл: замер с обоими блоками в одном сервере дал 500 на оба адреса.

Ошибка одна и та же - незадеплоенный или неверно названный файл, - а симптомы противоположные: в одном случае 500, в другом 404. Оба лечатся не переписыванием конфига, а взглядом в error_log: он в обоих случаях называет точный файл.

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

Наивное правило: «в этом location стоит proxy_pass, значит запросы уходят на бэкенд».

Если в одном блоке есть и try_files, и proxy_pass, то до бэкенда дело дойдёт только через последний аргумент. И вписать адрес прямо в try_files нельзя - назначением может быть URI, код или имя, но не адрес. Поэтому связка всегда выглядит так:

/etc/nginx/conf.d/shop.confhttp server
location / {
    try_files $uri @backend;
}

location @backend {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
}

proxy_set_header задаёт заголовок запроса к бэкендуadd_header - заголовок ответа клиенту); здесь приложение узнаёт имя сайта. Подробнее - в разделе про заголовки проксиРазбирается в разделе 7, глава «Заголовки: Host, X-Real-IP, X-Forwarded-*»: Что бэкенд перестаёт видеть.

Второе, о чём легко забыть: каждый кандидат стоит обращения к диску. try_files $uri $uri/ $uri.html $uri/index.html /fallback.html - это до четырёх системных вызовов на каждый запрос, которые ничего не найдут, прежде чем дело дойдёт до пятого варианта. На нагруженном сайте список кандидатов держат коротким, а не «на все случаи жизни».

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

  • 500 на живом сайте с целым конфигом. Ищи rewrite or internal redirection cycle в error_log: цель try_files возвращается в тот же блок, а файла нет.
  • 404 вместо ожидаемой заглушки. Цель ушла в другой location. Тот же дефект, другой симптом.
  • Запросы не доходят до бэкенда. В блоке есть try_files, и назначение у неё - не @имя.
  • nginx -t ругается на try_files с одним аргументом. Директива без назначения бессмысленна; это единственная ошибка главы, которую поймает проверка конфига.

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

try_files - главная директива отдачи статики, и почти всё, что с ней случается, разбирается по одному вопросу: что тут кандидат, а что назначение. Задавать его надо, глядя на последний аргумент, а не на первый.

Практическая привычка: при разборе чужого конфига читай try_files справа налево. Последний аргумент говорит, чем всё кончится, - вернётся код, уйдёт запрос на бэкенд или начнётся новый круг. Остальные аргументы после этого читаются как список «где сначала поискать», и их порядок обычно очевиден.

Теперь сам

1. Что сделает try_files $uri $uri/ /index.html =404;?

Ничего хорошего: назначением станет =404, а /index.html окажется кандидатом - то есть будет проверяться на диске. Назначение всегда одно и всегда последнее.

2. Почему try_files $uri; не соберётся?

Директиве нужно минимум два аргумента: без назначения она не отвечает на вопрос «а если не нашлось». Это единственная ошибка из главы, которую поймает nginx -t, - все остальные проявляются только на живых запросах.

3. В блоке location /images/ { try_files $uri /placeholder.png; } файл placeholder.png лежит в корне сайта. Что вернёт запрос на несуществующую картинку?

Заглушку - и код 200. Внутреннее перенаправление уходит на /placeholder.png, попадает в location //images/ этот URI не начинается), файл там есть. Обрати внимание на код: клиент получит 200, а не 404, - для картинок это обычно и нужно, для API было бы неприятным сюрпризом.

Главное

Все аргументы try_files, кроме последнего, - кандидаты, они проверяются на диске. Последний - назначение, оно не проверяется: =код, @имя или URI. Назначение-URI начинает обработку заново, и если новый URI попадает в тот же try_files - получается цикл и 500, а если в другой location - обычный 404.

Комментарии

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

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

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