Симптом обычно выглядит как ошибка приложения: внешний запрос приходит по HTTPS, а код считает его HTTP; все посетители в журнале имеют один адрес; после добавления proxy часть URL вдруг теряет префикс. Цена не в одном неверном заголовке. Неправильная схема ломает редирект или флаг cookie, общий адрес клиента делает бесполезным разбор ошибок, а случайный таймаут превращает медленный ответ в обвинение «бэкенд упал». Если сразу менять middleware или увеличивать лимит, команда лечит последний наблюдаемый слой, а не причину.
У reverse proxy простая, но важная роль: он принимает внешнее HTTP-соединение и создаёт отдельное соединение к приложению. Между ними меняются адрес peer, схема, часть заголовков, лимиты ожидания и журнал. Поэтому в апреле 2020 года я бы начал не с универсальной конфигурации, а с короткого контракта по hop-ам: что видит клиент, что обязан передать Nginx, что приложение имеет право считать доверенным и какой один маршрут докажет это на изолированном стенде. Ниже приведены учебные значения; они не были применены к browser, staging или production.
Прокси — не прозрачный провод
Пока клиент подключён к Nginx, для Nginx адрес клиента — адрес удалённой стороны этого соединения. Когда Nginx обращается к upstream, приложение видит уже Nginx как непосредственного соседа. Это не ошибка и не повод вручную подменять адрес в каждом контроллере. Это граница, на которой нужно явно договориться о передаваемых данных. Аналогично с TLS: TLS может завершаться перед приложением, а внутренний hop остаётся обычным HTTP. Приложение не может вычислить внешнюю схему из сокета, если proxy не передал ей согласованный признак.
В этой модели полезно разделить три класса данных. Первый — маршрут: метод, URI и Host, по которым приложение выбирает обработчик и строит ссылки. Второй — происхождение: схема и цепочка адресов, нужные для ограниченного набора решений. Третий — время: когда proxy смог подключиться к upstream, отправить запрос и дождаться следующего байта ответа. Смешать их легко: например, таблица в базе говорит, что URL правильный, а проблема на самом деле в Host, который по умолчанию у proxy не обязан совпадать с внешним host.
| Слой | Что он видит | Что может изменить | Что проверять |
|---|---|---|---|
| Клиент | внешний URL, статус и ответные заголовки | только свой запрос | метод, путь, ожидаемый статус и безопасный диагностический токен |
| Nginx | входной запрос и отдельный upstream-hop | Host, X-Forwarded-*, лимиты соединения и чтения | явные proxy_set_header, proxy_pass и формат access-log |
| Приложение | соединение от proxy и переданные заголовки | свою логику маршрута и журнал | какие заголовки допустимы только от доверенного proxy |
| Журнал | итог обработки на proxy | ничего не исправляет | статус, request_time и upstream-поля рядом, без секретов и пользовательских данных |
Эта таблица намеренно не называет конкретную топологию сети. Для одного проекта Nginx стоит прямо перед приложением; для другого перед ним уже есть балансировщик. Во втором случае $remote_addr на Nginx может обозначать предыдущий proxy, а не браузер. Подключать модуль realip имеет смысл только после того, как список доверенных источников определён отдельно. Его set_real_ip_from — это не косметическая настройка, а разрешение заменить адрес по заголовку.
Минимальная конфигурация как объект ревью
В учебной конфигурации ниже upstream намеренно локальный, а server_name не раскрывает ни одного рабочего имени. Она показывает форму договора, а не готовый фрагмент для копирования. Host передаётся явно, потому что у Nginx есть свои значения по умолчанию для заголовков proxied request. X-Forwarded-For накапливается через $proxy_add_x_forwarded_for, а X-Forwarded-Proto фиксирует схему hop-а, который пришёл на Nginx. Приложение должно принимать эти поля только из согласованной границы, а не из любого прямого HTTP-запроса.
# Учебная конфигурация: имена, адреса и значения не относятся к рабочему контуру.
upstream app_backend {
server 127.0.0.1:3000;
}
server {
listen 8080;
server_name _;
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 3s;
proxy_send_timeout 10s;
proxy_read_timeout 15s;
proxy_pass http://app_backend;
}
}
Три таймаута в примере отвечают на разные вопросы. proxy_connect_timeout ограничивает установление соединения с upstream. proxy_send_timeout относится к передаче запроса upstream между последовательными операциями записи. proxy_read_timeout относится к промежутку между чтениями ответа, а не к полной длительности ответа. Поэтому число 15s не является «временем работы API»: потоковый ответ может жить дольше, если upstream регулярно отдаёт байты, а тихий запрос может оборваться раньше, если приложение перестало отвечать. Значения здесь учебные, их нельзя переносить в рабочий контур без бюджета ожидания всего маршрута.
Один безопасный маршрут вместо широкого smoke-теста
Чтобы проверить контракт, не нужен полный прогон сайта. Нужен маршрут без пользовательских данных и с понятным ответом: например, /health или отдельный endpoint стенда. В запрос добавляется учебный токен, который можно увидеть и в журнале proxy, и в логе приложения, если приложение уже умеет его писать. Токен не становится средством аутентификации и не заменяет request ID; он всего лишь связывает две записи в контролируемом упражнении. Если таких журналов нет, сначала добавляют безопасный формат, а потом запускают проверку.
# Учебный запрос; не выполнялся в browser, staging или production.
# <PROXY_URL> и <HOST> — подстановки для изолированного стенда, не реальные адреса.
curl -i --max-time 5 \
'<PROXY_URL>/health' \
-H 'Host: <HOST>' \
-H 'X-Debug-Token: proxy-study-2020-04'
# Сверяем статус, Location, Set-Cookie и заголовок, который приложение
# записывает как схему или request token. Сам curl не доказывает путь до upstream.
После такого запроса нельзя делать вывод «приложение работает за proxy», если пришёл только 200. Проверяются четыре вещи: status и ответные заголовки снаружи, запись proxy с тем же токеном, запись приложения с этим же токеном и точное значение схемы или host, которое приложение использовало. Если внешнее соединение TLS, а приложение видит HTTP, это может быть корректно на внутреннем hop-е. Ошибкой становится не сам HTTP, а отсутствие согласованного сигнала, по которому код различает внешнюю схему.
Типовые симптомы не лечатся одним заголовком
Симптом «все адреса одинаковые» имеет минимум две причины. Либо приложение честно пишет peer address и видит Nginx, либо оно доверяет неподтверждённому X-Forwarded-For. Проверка разная: сначала определяем, есть ли прямой доступ к приложению и какой proxy имеет право дописывать заголовок. Действие — закрепить этот маршрут в конфигурации и в приложении, а не заменить адрес на первое значение из любой строки. RFC 7239 отдельно напоминает: forwarded-информация может быть изменена на пути и не становится достоверной без доверенной цепочки.
Симптом «редирект ведёт на http» тоже не означает, что Nginx сломан. Сначала проверяют, где завершается TLS, какой заголовок proxy передаёт и какое поле реально читает фреймворк. Затем сравнивают один учебный запрос до и после конфигурации. Если приложение использует один заголовок, а proxy передаёт другой, исправляется контракт на границе или адаптер приложения — но не добавляется ещё один независимый способ угадать схему. Так сохраняется возможность объяснить поведение новому человеку по четырём строкам конфигурации.
Маршрут внедрения и отката
- Записать один симптом в наблюдаемой форме: неверный Host, схема, адрес клиента, 502/504 или неожиданный timeout; назвать его цену для пользователя или разработки.
- Нарисовать реальные hop-ы без адресов и секретов: кто принимает внешний запрос, где заканчивается TLS и какой процесс является upstream для Nginx.
- Согласовать для приложения явный набор полей: Host, признак схемы, forwarded-цепочка и один безопасный диагностический токен; отдельно назвать, кто имеет право их выставлять.
- Собрать итоговую конфигурацию Nginx и проверить её синтаксис в изолированной среде; не менять одновременно route, приложение и значения таймаутов.
- Выполнить один учебный curl-маршрут на стенде, сопоставить записи proxy и приложения, затем изменить только ту строку конфигурации, которая объясняет расхождение.
- Сохранить ожидаемый результат и обратный шаг: какие поля и лимиты вернуть, если проверка не подтверждает гипотезу. Без этого увеличение таймаута остаётся необъяснимым обходом.
Граница должна быть маленькой и явной
Reverse proxy полезен тем, что отделяет внешний HTTP от процесса приложения. Но это разделение требует контракта: что передаётся, чему доверяют, сколько ждут и где виден результат. В апреле 2020 года для небольшого сервиса достаточно Nginx-конфигурации, одного диагностического маршрута, таблицы сигналов и короткого access-log. Не требуется усложнять схему service mesh, Kubernetes Ingress или распределённой трассировкой, чтобы перестать угадывать причину 502.
Эта статья не запускала Nginx, curl, browser, staging или production и не использует реальные hostnames, IP-адреса либо секреты. Конфигурация, адрес 127.0.0.1, путь журнала и таймауты — только учебные заполнители. Перед применением в проекте нужно отдельно подтвердить топологию, доверенные proxy, таймаут приложения, безопасный диагностический маршрут и правила хранения журналов.
Проверяемые источники
- Nginx: модуль ngx_http_proxy_module — первичная документация директив proxy_pass, proxy_set_header, proxy_connect_timeout, proxy_send_timeout и proxy_read_timeout
- Nginx: модуль ngx_http_realip_module — модуль меняет адрес клиента только по указанному заголовку и только для источников, явно объявленных доверенными через set_real_ip_from
- Nginx: модуль ngx_http_log_module — справочник log_format, access_log и переменной request_time; формат журнала должен быть частью диагностического контракта
- RFC 7239: Forwarded HTTP Extension — стандартный заголовок Forwarded, его связь с X-Forwarded-* и ограничение: данные заголовка нельзя считать достоверными без доверенной цепочки proxy
- RFC 7230: HTTP/1.1 Message Syntax and Routing — историческая для апреля 2020 года спецификация HTTP/1.1: маршрутизация, Host и границы между получателем и отправителем сообщения