Привет, Хабр!
Сегодня рассмотрим что на самом деле происходит при rewrite
в Nginx. Как работает связка rewrite ... break
, чем она отличается от rewrite ... last
, и как одно неловкое движение может превратить весь конфиг в тыкву.
rewrite в Nginx
Для тех, кто впервые сталкивается или подзабыл:
rewrite — директива, позволяющая изменять URI запроса на лету и при необходимости управлять дальнейшим потоком выполнения (полный перезапуск поиска нового location или продолжение обработки в текущем).
Синтаксис примерно такой:
rewrite <regex> <replacement> [flag];
Где
flag
может быть:last
,break
,redirect
,permanent
Но сегодня главным героем становятся break
и last
.
Почему существует несколько видов поведения rewrite?
Есть несколько стадий, когда Nginx пытается определить, как правильно подставить location, как изменить URI, где искать файл на диске (или передавать дальше в прокси), и прочее. В итоге разработчики сочли нужным дать нам разные флаги для детального управления этим процессом.
Когда мы говорим о rewrite ... break, мы имеем в виду:
«Применить указанное регулярное выражение, модифицировать URI и остаться в текущем location без повторного прохода по списку location»ов».
break
— это ручной тормоз: мы, мол, уже в нужном месте, и продолжаем дальнейшие директивы в той же конфигурационной области (том жеlocation
).
А когда встречаем rewrite ... last, мы говорим:
«Модифицировать URI и отправить запрос на новый цикл поиска подходящего location».
last
означает: «После изменения URI давай ещё раз прогрузим все правила, чтобы понять, не завалимся ли мы под другой location». Т.е Nginx опять пробежит по спискуlocation
от начала до конца (включая эффектыtry_files
,index
, etc.).
На первый взгляд звучит формально. Но есть важная разница: break
прерывает процесс location matching, мы остаёмся там, где были, а last
инициирует его заново.
Пример простейшего rewrite с break
Допустим, нужно обрезать /api/
и скормить приложению остаток строки.
server {
listen 80;
server_name example.com;
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:3000;
}
}
Идёт запрос на GET /api/users
. Регулярка ^/api/(.*)$
ловит все пути, начинающиеся с /api/
. Переписываем URI на /$1
, т. е. /users
. Используем break
, значит останемся в текущем location. В итоге Nginx отправит запрос на http://127.0.0.1:3000/users
.
Плюс break
в том, что весь движ происходит в рамках одного location. Если бы у нас после rewrite
в блоке location /api/
шли ещё какие‑то директивы (например, proxy_set_header
, add_header
), они бы отработали точно так, как мы и рассчитывали. Никаких дополнительных прогонов по другим location не будет.
Пример перенаправления с last
Однако что, если я хочу, чтобы после переписывания URI процесс поиска location начался заново, и в итоге запрос пришёл в совершенно иной location, прописанный, скажем, где‑то ниже?
server {
listen 80;
server_name example.com;
location /api/ {
rewrite ^/api/(.*)$ /app/$1 last; # last вместо break
}
location /app/ {
proxy_pass http://127.0.0.1:4000;
}
}
При GET /api/users
мы подменяем URI на /app/users
. last
говорит Nginx: «Я тут URI поменял, а теперь давай‑ка заново определим location». Nginx возвращается в список location и видит, что /app/
отлично подходит для /app/users
. Запрос уходит на http://127.0.0.1:4000/users
.
Так вот, если бы мы поставили break
, Nginx не пошёл бы искать подходящий location заново, остался бы в /api/
, и если там нет нужных настроек (а мы хотим реально обрабатывать в другом месте, скажем /app/
), то всё бы пошло не по плану.
Нюанс: rewrite ... last;
не означает «отправь 301/302 редирект наружу». Он не крутит клиентский редирект, он запускает внутренний цикл поиска location. Снаружи клиент не видит этих плясок, всё происходит внутри Nginx.
Как rewrite меняет URI внутри текущего location, и почему это может поломать root + alias
Есть тонкая штука: rewrite
может изменить $uri
(и, соответственно, пути, по которым Nginx будет искать файлы). Если мы потом в конфиге используем root
или alias
, есть риск, что итоговый путь на диске окажется не тем, который мы ожидаем.
Разница между root и alias
root
определяет корневой каталог внутри location, к которому прибавляется остаток пути (после того, как Nginx отрезал совпадениеlocation
).location /static/ { root /var/www/app; # Физический путь => /var/www/app/static/<filename> }
alias
заменяет часть пути location на указанный физический путь.location /images/ { alias /var/data/pictures/; # Физический путь => /var/data/pictures/<filename> }
Если вы внутри этого location делаете rewrite
, то реальный результат после подмены URI может конфликтовать с тем, что вы прописали в root
/alias
. Особенно если у вас break
, и вы остались в том же location, где alias
или root
были рассчитаны на другой URI.
Пример сломанного alias + rewrite с break
Допустим, у нас такой конфиг:
server {
listen 80;
server_name example.com;
location /files/ {
alias /var/data/;
# Хотим, чтобы /files/<filename> отдавал файл /var/data/<filename>
rewrite ^/files/(.*).jpg$ /images/$1.jpg break;
# Переписываем пути jpg-файлов, чтобы взять их из /images/<file>.jpg
# Но... мы не уходим в другой location, остаёмся здесь!
}
}
При запросе GET /files/photo.jpg
мы попадаем в location /files/
. rewrite
меняет URI на /images/photo.jpg
, но из‑за break
мы остаёмся в том же location /files/
.
Теперь у нас alias /var/data/
, а URI внезапно стал /images/photo.jpg
. Nginx будет пытаться отдать файл из /var/data/images/photo.jpg
(потому что alias
сопоставляет всю оставшуюся часть пути).
Если мы исходно хотели отдать файл /var/data/photo.jpg
, мы получим 404, потому что /var/data/images/photo.jpg
не существует.
Вот так rewrite ломает логику alias
. Мы сознательно переписали URI, но остались в том же location. И вот неожиданный результат: реальный путь, по которому идёт Nginx, превращается в /var/data/images/photo.jpg
.
Все это случается, потому что break
(в отличие от last
) не пускает запрос на повторный проход по location, а мы внутри этого location имеем alias
, которому достаётся уже новый URI.
Как решить?
Использовать
last
и создать отдельный location/images/
, гдеalias
илиroot
корректно настроен:location /files/ { rewrite ^/files/(.*).jpg$ /images/$1.jpg last; } location /images/ { alias /var/data/images/; }
Или не менять URI внутри этого location, а задать поведение напрямую:
location ~ ^/files/(.*)\.jpg$ { alias /var/data/$1.jpg; }
(можно использовать регулярное location, чтобы не городить rewrite).
Пример сломанного root + rewrite
Точно так же может ломаться и root
. Представим:
server {
listen 80;
server_name example.com;
location /files/ {
root /var/data;
rewrite ^/files/(.*)$ /somewhere/$1 break;
}
}
Если вы хотели отдавать файлы из /var/data/files/...
, то, переписывая URI на /somewhere/...
и оставаясь в том же location, вы получаете путь на диске /var/data/somewhere/...
.
Это не то, что планировалось, если ваша структура файлов в /var/data/files/...
.
Вопрос: «Но зачем тогда вообще использовать break, если есть last»?
Хороший вопрос, который часто возникает. Если кратко:
Производительность и упрощённая логика. Если вы уверены, что запрос уже попал в нужный location и не должен идти дальше,
break
экономит ресурсы, не гоняя движок по всем остальным location.Собранность конфигурации. Иногда удобно всё нужное для обработки конкретного запроса держать в одном
location
‑блоке: переписали URI, настроили заголовки, прикрутили кеширование, передали дальше — всё в одном месте.Специальные кейсы. Например, если нам важно, чтобы последующие строки в конфиге именно в этом location (провалидировать куки, установить специальные proxy_set_header и т. д.) были выполнены после
rewrite
.
Но если вы хотите перекинуть запрос в совершенно другой location (где другая настройка alias/root/proxy_pass), то без last
(или его аналогов типа return
с ручным кодом ответа) вам не обойтись.
Итог
Первое, что стоит держать в голове: директива break
завершает фазу rewrite
и не вызывает повторный поиск location
, тогда как last
запускает новый цикл определения location
.
Второе, при использовании root
или alias
внутри одного и того же location
любая неосторожная манипуляция с URI может привести к внезапному наложению путей. Физически на диске может получиться вовсе не то, чего вы ожидали, а в продакшене подобные сюрпризы — последнее, что хочется разруливать.
Третье, ключ к спокойствию — понимать, когда нужен повторный поиск location
(rewrite ... last
) и когда достаточно остаться в текущем (rewrite ... break
). При этом важно заранее продумать структуру каталогов и порядок объявлений location
, чтобы избежать конфликтов с путями и опустить лишние переписывания.
Минимальный чек‑лист:
Нужна ли смена
location
после переписывания URI?Да:
rewrite ... last;
Нет:
rewrite ... break;
Учтите реальную структуру каталогов. Если используется
alias
, не меняйте URI так, чтобы в итоге склеились нежелательные сегменты пути.Рассмотрите альтернативный вариант без rewrite. Иногда проще «в лоб» задать нужный
location
, чем громоздить серию переписаний.
А какой у вас опыт? Делитесь в комментариях.
Если вы работаете с конфигурацией серверов, балансировкой, прокси или просто хотите лучше понимать, как Nginx вписывается в инфраструктуру — эти открытые уроки могут быть полезным продолжением:
24 апреля — Docker в действии: как контейнеризация меняет аналитику данных
Контейнеры меняют архитектуру, но маршрутизацию никто не отменял. Поговорим о том, как это всё собирается воедино.28 апреля — Особенности интеграции 1С с веб-серверами
Интеграция — всегда нюансы. Особенно когда за веб-интерфейсом стоит Nginx и свои тонкости URI.29 апреля — Настройка кластера Elasticsearch
Где одна ошибка в конфиге — и полиндексации нет. Разбираемся, как не сломать прод, настраивая прокси и балансировку.