rewrite, return и канонизация URL

Рейтинг: 59.6% · 10 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

rewrite, return и канонизация URL

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if
  10. rewrite, return и канонизация URL (вы здесь)
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF
  39. Stream-модуль: проксирование TCP и UDP
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
Боль: один и тот же контент по десяти разным адресам

Открой логи любого живого сайта и посмотри, по каким URL приходят люди и боты. Ты увидишь зоопарк: site.ru/about, site.ru/about/, site.ru/About, http://site.ru/about, https://www.site.ru/about, site.ru/about?, site.ru//about, site.ru/index.php. Для человека это один и тот же раздел. Для поискового робота это разные документы с одинаковым содержимым - дубли. Дубли размазывают вес страницы, плодят мусор в индексе, ломают аналитику и открывают дыры безопасности (вспомни про подмену Host или протаскивание лишних слешей в путь). Задача шлюза - привести любой входящий адрес к ОДНОЙ канонической форме и отдать всё остальное через редирект. Это и называется канонизация URL.

В nginx за управление адресами отвечают две директивы: return и rewrite. Большинство людей по привычке тащат rewrite с регэкспами туда, где хватило бы одной строки return. В этом уроке разберём, почему return почти всегда лучше rewrite, чем отличаются коды 301/302/307/308, как сделать nginx 301 редирект правильно и как собрать канонизацию по образцу эталонного gateway - через map $needs_slash и return, БЕЗ леса if. И отдельно - почему в 2026 году минимизация rewrite-правил это не вкусовщина, а прямой ответ на CVE-2026-42945.

Изображение

return: самый быстрый nginx редирект

return делает ровно одно: немедленно завершает обработку и отдаёт клиенту указанный код (и опционально тело или URL). Никаких регэкспов, никакого внутреннего цикла. Синтаксис прост.

Код: Выделить всё

# редирект на HTTPS - классика
server {
    listen 80;
    server_name cyberlake.ru www.cyberlake.ru;
    return 301 https://cyberlake.ru$request_uri;
}

# отдать код без тела
location = /old-api { return 410; }

# отдать готовый ответ прямо из конфига
location = /healthz { return 200 "ok\n"; }
Обрати внимание на $request_uri в редиректе на HTTPS. Это путь ВМЕСТЕ со строкой запроса (/path?a=1), уже в том виде, в каком пришёл от клиента. Если написать $uri, потеряешь query string, и редирект съест параметры - частая и обидная ошибка. Запомни разницу: $uri это нормализованный путь БЕЗ аргументов (и он меняется при внутренних редиректах), $request_uri это сырой исходный URI как есть. Для nginx return 301 на другой хост почти всегда нужен именно $request_uri.

Когда в return указан полный URL со схемой (https://...), nginx понимает, что это внешний редирект, и ставит код 301/302 с заголовком Location. Если URL начинается с /, тоже Location, но относительный. А вот return с одним только кодом (return 403) отдаёт ответ на месте, не редиректит.

rewrite: когда регэксп всё-таки нужен

rewrite меняет URI по регулярному выражению. Он нужен там, где адрес надо РАЗОБРАТЬ и собрать заново: перенос целой ветки путей, ЧПУ поверх старого скрипта, склейка групп. Синтаксис: rewrite регэксп замена [флаг].

Код: Выделить всё

# перенос раздела: /blog/2020/foo -> /articles/foo, внешний 301
rewrite ^/blog/\d+/(.+)$ /articles/$1 permanent;

# внутреннее переписывание под скрипт, клиент адрес НЕ видит
rewrite ^/news/(\d+)$ /index.php?id=$1 last;
Флаги решают всё:
  • permanent - внешний редирект 301, клиент получает Location и новый адрес в строке браузера.
  • redirect - внешний редирект 302 (временный).
  • last - внутреннее переписывание, после него nginx заново ищет location для нового $uri и обработка продолжается там. URL в браузере не меняется.
  • break - внутреннее переписывание, но поиск location НЕ повторяется, обработка остаётся в текущем блоке. Дальнейшие rewrite в этом блоке игнорируются.
Разница last vs break - источник вечной путаницы. last говорит "перепиши и начни поиск location сначала". break говорит "перепиши и заканчивай с rewrite-фазой, оставайся здесь". Если внутри location ~ написать rewrite ... break, ты фиксируешь обработку в этом блоке - удобно, чтобы не словить новый виток поиска. Если написать last, рискуешь уйти в другой location, а оттуда снова сюда - и получить цикл.

Порядок выполнения тоже неинтуитивный. rewrite, return, if, set, break - это директивы модуля ngx_http_rewrite_module, и они выполняются СВЕРХУ ВНИЗ на этапе rewrite-фазы, ДО выбора финального location-обработчика. Сначала отрабатывают директивы на уровне server, потом nginx выбирает location, потом отрабатывают директивы внутри него. Поэтому return на уровне server сработает раньше, чем любой location, - это и делает редирект на HTTPS таким дешёвым.

Почему return предпочтительнее rewrite (и при чём тут безопасность)

Три причины, по нарастанию важности.

Производительность. return не запускает регэксп-движок и не инициирует внутренний редирект - он просто пишет ответ. rewrite на каждый запрос прогоняет PCRE, а с флагом last ещё и заставляет nginx заново проходить фазу выбора location. На горячем пути шлюза это лишние такты на ровном месте.

Читаемость. return 301 https://host$request_uri; читается за секунду. Цепочка из пяти rewrite с группами $1, $2 и флагами требует медитации, чтобы понять, что и куда уедет, и не словит ли цикл.

Безопасность - и это главное в 2026. В мае 2026 в реальных атаках эксплуатировался CVE-2026-42945 (его прозвали "NGINX Rift") - heap buffer overflow прямо в ngx_http_rewrite_module, затрагивающий версии от 0.6.27 до 1.30.0. Неаутентифицированный злоумышленник специально сформированным запросом мог уронить worker, а в худшем сценарии добиться удалённого выполнения кода. Особенно опасны конфигурации с rewrite/if/set, использующими БЕЗЫМЯННЫЕ захваты $1, $2. Выводов два. Первый: держи nginx и модули обновлёнными (фикс в 1.30.1 / 1.31.x). Второй, архитектурный: минимизируй rewrite-правила. Чем меньше регэксп-логики в конфиге, тем меньше поверхность атаки. Каждый rewrite, который можно заменить на return или на map, надо заменить. Это ровно та практика, что зашита в эталонном gateway ngx-trace-gateway: вся канонизация там сделана через map и return, а не через лес if с регэкспами.

301 vs 302 vs 307 vs 308: метод имеет значение

Код редиректа - не косметика. Он говорит браузеру и роботу две вещи: насколько это навсегда и можно ли менять HTTP-метод. Запутаешься тут - сломаешь либо SEO, либо POST-формы.
  • 301 Moved Permanently - навсегда. Поисковики переносят вес страницы на новый адрес, браузеры АГРЕССИВНО кешируют 301 (иногда навсегда). Ставь только когда уверен, что не передумаешь. Историческая тонкость: при 301/302 браузеры могут поменять POST на GET.
  • 302 Found - временно. Вес не переносится, кеш мягче. Для временных заглушек, A/B, обслуживания.
  • 307 Temporary Redirect - временно, но с ГАРАНТИЕЙ сохранения метода и тела. POST остаётся POST. Это и есть единственное отличие от 302.
  • 308 Permanent Redirect - навсегда И с сохранением метода. POST на старый адрес уедет POST-ом на новый.
Правило на пальцах: для канонизации обычных GET-страниц (HTTPS, www, слеши) - 301, это то, чего ждут Яндекс и Google для nginx canonical url. Для редиректа API-эндпойнтов, куда летят POST/PUT, - 308 (или 307 для временного), иначе клиент молча потеряет тело запроса. rewrite, кстати, умеет ставить только 301 (permanent) и 302 (redirect) - для 307/308 нужен именно return.

Код: Выделить всё

# API переехал, метод обязан сохраниться
location = /v1/orders { return 308 https://api.cyberlake.ru/v2/orders; }
Практика: канонизация через map $needs_slash + return (как в эталоне)

Теперь соберём боевую канонизацию по образцу ngx-trace-gateway. Идея: один map вычисляет, нужен ли пути завершающий слеш, а дальше короткие return разруливают остальное - без единого if с регэкспом в горячем пути.

Сначала на уровне http определяем, каким путям слеш нужен, а каким нет. Файлам с расширением (.css, .js, .png), корню, путям, уже оканчивающимся на слеш, и служебным эндпойнтам слеш не нужен. Чистым "директорным" путям - нужен.

Код: Выделить всё

# в http-контексте (globals/default по образцу эталона)
map $uri $needs_slash {
    default                 1;     # чистый путь без слеша -> добавить
    ~\.[a-zA-Z0-9]+$        0;     # есть расширение -> файл, не трогаем
    ~/$                     0;     # уже оканчивается слешем
    /                       0;     # корень
    /nginx_status           0;     # служебное
}
Теперь в server-блоке - сама канонизация. Порядок важен: схема и хост первыми (это и есть nginx 301 редирект на канонический origin), потом чистка пути.

Код: Выделить всё

server {
    listen 443 ssl;
    http2 on;
    server_name cyberlake.ru www.cyberlake.ru;

    # 1. www -> без www, всегда на канонический хост и схему
    if ($host = www.cyberlake.ru) {
        return 301 https://cyberlake.ru$request_uri;
    }

    # 2. добавить завершающий слеш чистым путям (значение из map)
    location / {
        if ($needs_slash) {
            return 301 https://cyberlake.ru$uri/;
        }
        try_files $uri $uri/ =404;
    }

    # 3. убрать index.php из адреса
    location = /index.php {
        return 301 https://cyberlake.ru/;
    }
}
Здесь if применён ровно так, как его безопасно применять, - с return внутри (это поддерживаемый паттерн, в отличие от if с другими директивами, который славится сюрпризами). Один map снимает всю ветвящуюся логику "файл это или папка". Двойные слеши, пустой ? и приведение к нижнему регистру в эталоне решаются тем же приёмом - коротким map плюс return, а не каскадом rewrite. Это и есть смысл фразы "канонизация через return, без if-леса": ветвление уезжает в декларативный map, а исполнение - в дешёвый return.

error_page и internal redirect: красивые ошибки без дублей

Канонизация - частный случай внутренних редиректов, и тот же механизм работает для страниц ошибок. Когда nginx ловит 404 или 502, директива error_page инициирует ПОЛНОСТЬЮ НОВЫЙ внутренний запрос на указанный URI - это и есть internal redirect. По образцу http_server.conf из эталона ошибки отдаём через именованные @-локации, помеченные internal, чтобы снаружи на них нельзя было постучаться напрямую.

Код: Выделить всё

recursive_error_pages on;

error_page 404 = @error404;
error_page 403 = @error403;
error_page 500 502 503 504 = @error50x;

location @error404 {
    internal;
    root /var/www/errors;
    try_files /404.html =404;
}
location @error50x {
    internal;
    root /var/www/errors;
    try_files /50x.html =500;
}
Зачем recursive_error_pages on? По умолчанию nginx разворачивает error_page только ОДИН раз. Если при обработке страницы ошибки случилась ещё одна ошибка (например, сам 50x.html не нашёлся), без рекурсии nginx упрётся и отдаст дефолтную страницу. С recursive_error_pages on он позволит цепочке развернуться дальше - но осторожно, защита от бесконечного цикла есть, и слишком длинная цепочка оборвётся. Знак "=" в error_page 404 = @error404 означает "поменяй код ответа на тот, что вернёт обработчик". Без "=" клиент получит исходный 404, а @-локация лишь отрисует тело; с "=" итоговый код определит сама локация (в примере try_files ... =404 вернёт честный 404). Именованные локации (@error404) достижимы ТОЛЬКО через внутренний редирект и сохраняют исходный URI - снаружи их не вызвать, что и нужно.

Типичные грабли
  • $uri вместо $request_uri в редиректе на хост. Потеряешь query string. На канонический хост/схему - всегда $request_uri.
  • Циклы в rewrite. rewrite ... last, который снова попадает в location, переписывающий обратно, - бесконечный виток. nginx оборвёт его после 10 итераций с 500 и записью "rewrite or internal redirection cycle" в error_log. Лечится переходом на break или на return.
  • if как швейцарский нож. "If is evil" - if с любыми директивами кроме return/rewrite ведёт себя непредсказуемо (особенно с proxy_pass и add_header внутри). В канонизации используй if ТОЛЬКО с return, а ветвление выноси в map.
  • Безымянные захваты $1/$2 в 2026. Кроме общей рекомендации минимизировать rewrite, для оставшихся правил предпочитай ИМЕНОВАННЫЕ группы (?<name>...) - это снижает риск по CVE-2026-42945 и просто читается лучше.
  • 301 поставил - не откатишь. Браузер закеширует 301 надолго. Пока проверяешь логику, ставь 302/307, и только убедившись - меняй на 301/308.
  • Забыл add_header после редиректа. return завершает обработку, заголовки из родительского контекста на ответ редиректа могут не попасть - не рассчитывай на HSTS внутри 80-го server, который только редиректит.
Мини-лаба: собери канонизацию руками
  • Шаг 1. Подними тестовый server на 8081 с server_name localhost; добавь блок: location = /index.php { return 301 /; }. Проверь: curl -i http://localhost:8081/index.php - жди 301 и Location: /.
  • Шаг 2. Добавь map $uri $needs_slash из урока в http-контекст и location / с if ($needs_slash) { return 301 /$uri/; } (для теста относительный путь). Запрос curl -i http://localhost:8081/about должен отдать 301 на /about/, а curl -i http://localhost:8081/style.css - НЕ должен (расширение). nginx -t перед каждым reload.
  • Шаг 3. Сделай server на 80 с единственной строкой return 301 https://$host$request_uri; и проверь curl -i "http://localhost/page?x=1" - в Location обязан быть ?x=1. Убери из строки $request_uri, замени на $uri, повтори - увидишь, как пропал query string.
  • Шаг 4. Подключи error_page 404 = @error404; recursive_error_pages on; и internal @-локацию с try_files /404.html =404. Запроси несуществующий URL и убедись, что отдаётся твоя страница с кодом 404 (curl -i, смотри первую строку статуса).
  • Шаг 5. Намеренно создай цикл: rewrite ^/loop /loop last; в location /. Сделай запрос, открой error_log и найди строку про "rewrite or internal redirection cycle" и 500 - чтобы знать этот симптом в лицо. Потом убери.
Контрольные вопросы
  • Почему для редиректа на канонический хост в return пишут $request_uri, а не $uri? Что сломается при подмене?
  • Клиент шлёт POST на старый адрес эндпойнта, который переехал. Какой код редиректа поставишь и почему НЕ 301?
  • Чем отличаются флаги last и break у rewrite и в каком случае last приводит к циклу?
  • Зачем @-локации для ошибок помечают internal и что даёт recursive_error_pages on?
Итог

return - твой основной инструмент управления URL: быстрый, читаемый и с минимальной поверхностью атаки. rewrite оставляй только там, где адрес реально надо разобрать регэкспом, и тогда - именованные группы и обновлённый nginx (помни про CVE-2026-42945). Канонизацию строй декларативно: map $needs_slash плюс короткие return, без if-леса, ровно как в эталонном gateway. Различай 301/302/307/308 по двум осям - навсегда ли и сохраняется ли метод: GET-страницы канонизируй 301-м, POST-эндпойнты переноси 308/307-м. Ошибки отдавай через internal @-локации с recursive_error_pages on. Один адрес - одна каноническая форма: чисто для SEO, безопасно для шлюза.
👍6 ❤️2 🔥1 😄 🤔1
Аватара пользователя
bash9
Сообщения: 1
Зарегистрирован: 04 июн 2026, 20:27

Re: rewrite, return и канонизация URL

Сообщение bash9 »

Вопрос по 308: если у меня API на POST переехал на новый домен, 308 точно сохранит тело? А то на 301 у меня клиенты молча теряли json и я неделю ловил пустые заказы.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
envoy98
Сообщения: 1
Зарегистрирован: 29 май 2026, 20:12

Re: rewrite, return и канонизация URL

Сообщение envoy98 »

До этого тащил везде rewrite ^/(.*)$ ... permanent по привычке, а оказывается одна строка return 301 https://host$request_uri решает то же самое и без регэкспа. Спасибо, переписал конфиг.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Переменные, map и блок if
Следующая глава →
Работа с заголовками: add_header и proxy_set_header

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
Похожие запросы: nginx rewrite и редиректы

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость