Nginx как API-шлюз: маршрутизация и заголовки
Nginx может направлять разные API-маршруты в отдельные сервисы, сохранять исходный IP и передавать протокол запроса. Ошибка в одном слеше или заголовке меняет URI и ломает авторизацию, логи и трассировку.

Nginx передаёт запросы не туда из-за двух символов в proxy_pass: наличия или отсутствия завершающего слеша. К этому добавляются заголовки Host, X-Forwarded-For и X-Forwarded-Proto — именно по ним приложение понимает, какой адрес увидел клиент и был ли запрос защищён HTTPS. Ниже — практическая схема для Nginx Open Source 1.25.x: три способа маршрутизации, рабочие фрагменты конфигурации и ошибки, которые проявляются уже за прокси.
Что должен делать API-шлюз
В этой схеме Nginx принимает запросы на одном публичном домене и отправляет их в разные внутренние сервисы:
/api/v1/users/— сервис пользователей;/api/v1/orders/— сервис заказов;/api/v1/health— отдельная проверка доступности.
Шлюз не обязан знать бизнес-логику. Его задача — выбрать upstream, сохранить нужные свойства запроса, не пропустить лишние служебные заголовки и вернуть клиенту корректный ответ.
Самая дорогая ошибка в API-шлюзе — не падение Nginx, а незаметно изменённый запрос: сервис отвечает 404, считает HTTPS обычным HTTP или записывает в журнал адрес самого прокси.
Сноска. Upstream — группа серверов, в которую Nginx передаёт запрос. Это может быть один контейнер, несколько экземпляров приложения или DNS-имя внутреннего сервиса.
Три способа выбрать сервис
Выбор зависит от того, где живёт правило маршрутизации. Для API с устойчивыми публичными URL обычно достаточно префиксов в location. Более сложные схемы стоит вводить только при понятной цене их сопровождения.
| Подход | Когда применять | Цена решения |
|---|---|---|
Префикс location | Разные сервисы имеют разные части URI | Просто читать и проверять; нужно внимательно работать со слешем в proxy_pass |
Точный или регулярный location | Нужно отдельно обработать один endpoint или набор похожих путей | Сложнее предсказать приоритет правил; регулярные выражения легко расширить слишком широко |
map по заголовку или параметру | Маршрут зависит от версии, tenant или другого признака запроса | Правило становится менее очевидным, а проверка входных значений — обязательной |
Маршрутизация по префиксу
Начните с отдельного upstream для каждого внутреннего сервиса. В реальной конфигурации блоки upstream размещают в контексте http, а server — внутри него.
upstream users_backend {
server users:8080;
}
upstream orders_backend {
server orders:8080;
}
server {
listen 443 ssl;
server_name api.example.com;
location /api/v1/users/ {
proxy_pass http://users_backend;
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 X-Request-ID $request_id;
proxy_set_header Connection "";
}
location /api/v1/orders/ {
proxy_pass http://orders_backend;
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 X-Request-ID $request_id;
proxy_set_header Connection "";
}
location = /api/v1/health {
proxy_pass http://users_backend;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
}
}
В этом варианте proxy_pass указан без URI после имени upstream. Nginx передаст сервису исходный нормализованный URI: запрос к /api/v1/users/42 останется таким же. Это удобно, если приложение само знает о публичном префиксе.
Главная ловушка: слеш в proxy_pass
Если после адреса upstream есть URI, Nginx заменяет часть исходного URI, которая совпала с location. Поэтому эти два варианта отправят один и тот же клиентский запрос по-разному.
location /api/v1/users/ {
proxy_pass http://users_backend;
}
# В сервис уйдёт: /api/v1/users/42
location /api/v1/users/ {
proxy_pass http://users_backend/;
}
# В сервис уйдёт: /42
Первый вариант подходит для сервиса, который обслуживает публичный путь целиком. Второй — когда приложение ожидает корневой маршрут без префикса. Если backend получает 404 после переноса за Nginx, первым делом сравните ожидаемый URI и фактическое правило со слешем.
Не смешивайте в одном маршруте несколько преобразований: удаление префикса, переписывание пути и передачу параметров лучше проверять по отдельности. Инвалидация кеша и балансировка между зонами — отдельные темы, здесь важны только маршрут и заголовки.
Какие заголовки передавать приложению
Host и исходный адрес
При проксировании Nginx по умолчанию меняет ряд служебных заголовков. Явно задайте Host, если приложение выбирает tenant, формирует абсолютные URL или проверяет домен. Переменная $host безопаснее для такой задачи, чем ручная строка из клиентского заголовка.
X-Real-IP содержит адрес непосредственного клиента для приложения, а X-Forwarded-For — цепочку адресов прокси. Переменная $proxy_add_x_forwarded_for добавляет текущий адрес к существующей цепочке. Если Nginx доступен напрямую из интернета, нельзя бездумно считать первый адрес в этой цепочке достоверным: клиент может прислать собственное значение. Доверять цепочке нужно только от известных промежуточных прокси.
Протокол и идентификатор запроса
X-Forwarded-Proto нужен, когда TLS завершается на Nginx, а внутренний сервис получает обычный HTTP. Без него приложение может генерировать ссылки с http://, зацикливать редирект на HTTPS или неправильно выставлять cookie.
$request_id позволяет связать запись Nginx с логом backend. Это не полноценная распределённая трассировка: для нескольких сервисов понадобятся согласованные правила передачи и обработки идентификатора.
Connection и hop-by-hop-заголовки
Заголовок Connection относится к соединению между соседними участниками, а не к сквозному API-запросу. Пустое значение в конфигурации выше не передаёт его в backend. Не переносите вручную все служебные заголовки из клиентского запроса: часть из них имеет смысл только для одного соединения.
Заголовок Authorization обычно передаётся прокси дальше автоматически. Не нужно добавлять его в конфигурацию без причины. Если перед вами отдельный security-прокси или несколько уровней Nginx, проверьте реальный запрос на backend и убедитесь, что заголовок не удаляется промежуточным правилом.
Маршрутизация по заголовку или версии
Иногда путь одинаковый, а сервис выбирается по версии API или признаку окружения. Для этого подходит map. Директива должна находиться в контексте http, а не внутри location.
map $http_x_api_version $selected_backend {
default users_backend;
"2" users_v2_backend;
}
upstream users_v2_backend {
server users-v2:8080;
}
server {
listen 443 ssl;
server_name api.example.com;
location /api/users/ {
proxy_pass http://$selected_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
}
}
Такой подход требует строгого списка допустимых значений. Не подставляйте в адрес upstream произвольный заголовок или параметр клиента. Иначе правило превращается в неконтролируемый выбор назначения и усложняет защиту внутренней сети. Для публичного API версия в URI обычно прозрачнее: /api/v1/ и /api/v2/ проще видеть в логах и тестах.
Что ломается при проверке
- Backend получает не тот путь. Сравните
proxy_passсо слешем и без него. Добавьте временный лог или посмотрите URI в access log приложения. - В приложении всегда HTTP. Проверьте
X-Forwarded-Protoи настройку доверия к proxy headers на стороне фреймворка. Одного заголовка недостаточно, если приложение его игнорирует. - В логах адрес Nginx. Убедитесь, что backend читает
X-Real-IPилиX-Forwarded-For, а сеть между прокси и сервисом не позволяет любому клиенту подделывать доверенный заголовок. - Правило не срабатывает. Проверьте приоритет
location: точное совпадение, обычный префикс и регулярное выражение выбираются по разным правилам. - После reload старый конфиг. Перед перезагрузкой выполните
nginx -t. Если проверка прошла, используйте штатный reload процесса и затем отправьте тестовый запрос с уникальным значением, например версией API.
Когда какой подход выбирать
Используйте префиксный location, если разные сервисы уже разделены URL. Выбирайте точный location для healthcheck или одного особо важного endpoint, которому нужны отдельные таймауты и заголовки. map оправдан, когда один и тот же путь действительно должен вести в разные backend по ограниченному набору значений.
Не скрывайте от приложения публичный префикс только потому, что так короче URL внутри контейнера. Удаление префикса полезно, когда backend не должен знать о внешней структуре, но тогда абсолютные ссылки, callback URL и документация API должны быть проверены отдельно.
Что делать дальше
Составьте таблицу маршрутов: внешний URI, ожидаемый URI в backend, upstream и обязательные заголовки. Затем добавьте интеграционный тест для каждого маршрута с проверкой пути, кода ответа, X-Request-ID и протокола. В проде смотрите не только на 5xx, но и на расхождение между access log Nginx и логом сервиса: оно быстрее всего показывает ошибку маршрутизации.
Транскрибатор встреч: Запись созвона или диктофона — в текст с разделением по спикерам, протоколом и списком задач.
Спросить за 25 ₽Источники: NGINX documentation: proxy_pass, NGINX documentation: HTTP proxy module
Частые вопросы
Нужно ли всегда добавлять завершающий слеш в proxy_pass?
Почему backend видит HTTP, если клиент подключился по HTTPS?
Можно ли считать первый адрес в X-Forwarded-For адресом клиента?
Почему location не выбирается, хотя URI подходит под правило?
Что проверить, если после добавления слеша сервис начал отвечать 404?
Можно ли передавать клиентский X-Request-ID без проверки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.