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

# теория · шаг 2 из 6

return: коды, тела, редиректы

Коротко

return отвечает сам, не трогая ни диск, ни бэкенд. Форм у неё четыре, и четвёртая - голый адрес без кода - молча даёт 302, а не 301. Работает она на фазе rewrite, то есть раньше deny, limit_req и auth_basic: строка return 200 в тестовом блоке отменяет всю защиту, которую ты рядом настроил. Для редиректа берут $request_uri, а не $uri: второй теряет строку параметров.

Знаешь эту директиву - листай до «Теперь сам». Разборы нужны, если ловил 200 там, где ждал 403.

Вот блок из настоящего конфига. Как думаешь, что получит клиент с чужого адреса?

/etc/nginx/conf.d/shop.local.confhttp server
location /вход {
    deny all;
    return 200 "проверка связи\n";
}

deny all стоит первой строкой и написан явно. На стенде 1.31.5 этот блок отдаёт 200 и текст проверки. Не 403.

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

Представь турникет в бизнес-центре и охранника перед ним. Охранник стоит раньше турникета: если он выдаёт пропуск сам, до турникета человек просто не доходит - и неважно, что турникет настроен и работает. return в nginx стоит на такой же позиции: он срабатывает на фазе rewrite, а deny, лимиты и пароль живут на фазах, которые идут позже.

Механизм: где на конвейере стоит return

rewrite return здесь лимиты доступ try_files контент ответ уходит отсюда сюда запрос уже не доедет
Ответ уходит с первой фазы конвейера, остальные не выполняются

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

Правило

Второй аргумент читается по коду. Код из редиректных (301, 302, 303, 307, 308) - значит адрес. Любой другой код - значит тело ответа. Кода нет вовсе - значит адрес, и ответ будет 302.

return 403;                                  # код без тела
return 200 "pong\n";                         # код и тело
return 301 https://shop.local$request_uri;   # код и адрес
return https://shop.local/;                  # адрес без кода = 302

Разбор: четыре формы на стенде

Проверено на 1.31.5, по одному location на форму:

запись строка ответа Location тело
return 200 "тело" 200 OK нет тело
return 404 404 Not Found нет стандартная страница
return 301 /new 301 Moved Permanently http://localhost/new страница редиректа
return https://short.example/ 302 Moved Temporarily https://short.example/ страница редиректа
return 444 соединения нет - curl отвечает кодом 52
return 418 418 без названия нет пусто

Две строки заслуживают внимания. Голый адрес дал 302, хотя писавший почти всегда имеет в виду постоянный переезд. И относительный путь /new уехал клиенту как абсолютный http://localhost/new - адрес собран из схемы, Host и пути, поэтому за чужим терминатором TLS туда попадёт http://, если не поправить absolute_redirect или не написать адрес целиком.

Разбор: почему редирект теряет метки маркетинга

Два одинаковых на вид редиректа, замер на одном и том же запросе /red?x=1:

location = /red-uri { return 301 $uri; }
location = /red-req { return 301 $request_uri; }
$  команда
curl -sI 'http://localhost/red-uri?x=1' | grep -i location
curl -sI 'http://localhost/red-req?x=1' | grep -i location
↳  выводдве команды выше, из каждой оставлена строка Location
Location: http://localhost/red-uri
Location: http://localhost/red-req?x=1

$uri - это нормализованный путь без строки параметров, да ещё и изменённый предыдущими перезаписями. $request_uri - то, что клиент прислал, целиком. Редирект на $uri работает: страница открывается, код 301, в мониторинге всё зелено. Пропадает только ?utm_source=..., и замечают это через месяц по отчёту, в котором переходы из рассылки стали нулевыми.

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

  • Заглушка отменяет защиту. deny all рядом с return 200 не срабатывает: замер даёт 200 против 403 у того же блока без return. То же с limit_req и auth_basic.
  • Проверка здоровья считает себя проверкой. location = /healthz { return 200 "ok"; } отвечает «ok», даже когда бэкенд лежит: до бэкенда запрос не идёт. Это нормально для проверки самого nginx и бесполезно как проверка приложения.
  • error_page 404 = 200 /empty.json не грузится. nginx отвечает invalid value "=": код прирастает к знаку равенства без пробела, верная запись - =200. Форма = @name с пробелом при этом законна, потому что там кода нет.
  • Отсутствующая страница ошибки не роняет сервер. error_page 404 /нет-такой.html при отсутствующем файле отдаёт обычную стандартную страницу 404, а в error_log появляется open() ... failed (2: No such file or directory). Ни цикла, ни 500.

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

Три места, где return - самый честный инструмент.

/etc/nginx/conf.d/shop.local.confhttp server
location = /healthz {
    access_log off;
    default_type application/json;
    return 200 '{"status":"ok"}';
}

Проверка здоровья ходит раз в секунду - за сутки это под сотню тысяч строк в логе, поэтому access_log off тут не украшение.

/etc/nginx/conf.d/00-default.confhttp
server {
    listen 80 default_server;
    server_name _;
    return 444;
}

Блок по умолчанию, который закрывает соединение молча. Сканер видит не сайт, а отсутствие сервера. От целевой атаки это не защищает, но убирает твой сайт из чужих списков «что живёт на этом адресе».

И третье - переезд домена одной строкой, без регулярки на каждый запрос:

return 301 https://новый-домен.ru$request_uri;

Теперь сам

Блок отвечает не тем, чего от него ждут. Что увидит клиент и почему?

location /api/ {
    limit_req zone=one burst=5;
    return 200 "api\n";
}

Ответ: все запросы получат 200, лимит не сработает ни разу. return живёт на фазе rewrite, limit_req - на preaccess, а до неё дело не дойдёт. Чтобы лимит заработал, содержимое должен отдавать кто-то на фазе контента: proxy_pass, try_files или файл с диска.

Главное

return отвечает раньше доступа и лимитов - поэтому заглушка с return 200 делает соседние deny и limit_req бессмысленными. Адрес без кода даёт 302. В редиректе берут $request_uri, иначе теряется строка параметров.

Комментарии

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

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

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