Как стать автором
Обновить
599.13
OTUS
Цифровые навыки от ведущих экспертов

Nginx rewrite: когда нужен break, а когда last

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров1.7K

Привет, Хабр!

Сегодня рассмотрим что на самом деле происходит при 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.

Как решить?

  1. Использовать last и создать отдельный location /images/, где alias или root корректно настроен:

    location /files/ {
        rewrite ^/files/(.*).jpg$ /images/$1.jpg last;
    }
    
    location /images/ {
        alias /var/data/images/;
    }
  2. Или не менять 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»?

Хороший вопрос, который часто возникает. Если кратко:

  1. Производительность и упрощённая логика. Если вы уверены, что запрос уже попал в нужный location и не должен идти дальше, break экономит ресурсы, не гоняя движок по всем остальным location.

  2. Собранность конфигурации. Иногда удобно всё нужное для обработки конкретного запроса держать в одном location‑блоке: переписали URI, настроили заголовки, прикрутили кеширование, передали дальше — всё в одном месте.

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

Минимальный чек‑лист:

  1. Нужна ли смена location после переписывания URI?

    • Да: rewrite ... last;

    • Нет: rewrite ... break;

  2. Учтите реальную структуру каталогов. Если используется alias, не меняйте URI так, чтобы в итоге склеились нежелательные сегменты пути.

  3. Рассмотрите альтернативный вариант без rewrite. Иногда проще «в лоб» задать нужный location, чем громоздить серию переписаний.

А какой у вас опыт? Делитесь в комментариях.


Если вы работаете с конфигурацией серверов, балансировкой, прокси или просто хотите лучше понимать, как Nginx вписывается в инфраструктуру — эти открытые уроки могут быть полезным продолжением:

Теги:
Хабы:
+6
Комментарии2

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS