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

# теория · шаг 5 из 7

Заголовки, которые прокси обязан менять

Коротко

Заголовки могут описывать запрос целиком или отдельное соединение. При проксировании проверь, что получил бэкенд: назначение заголовка и фактическая обработка конкретной сборкой не всегда совпадают.

  • Заголовки делятся на сквозные (для конечного получателя) и участковыеУчастковый заголовокЗаголовок, который описывает текущий участок пути, а не сам запрос. Стандарт называет основные (Connection, Keep-Alive, Upgrade, Transfer-Encoding, TE, Trailer и два Proxy-) и разрешает объявить участковым любой другой через Connection. nginx при проксировании сам снимает Connection, Keep-Alive, Upgrade и TE - именно поэтому WebSocket через nginx требует поставить Upgrade и Connection заново., hop-by-hop (для соседа по цепочке). Участковые по правилам HTTP относятся к одному соединению.
  • Стандарт называет основные участковые: Connection, Keep-Alive, Upgrade, Transfer-Encoding, TE, Trailer, Proxy-Authenticate, Proxy-Authorization. nginx из запроса сам снимает четыре - Connection, Keep-Alive, Upgrade и TE, - а Trailer и оба Proxy-* пересылает как есть (замер на 1.31.8).

Если разница сквозных и участковых заголовков и правило «всё или ничего» знакомы - листай до «Теперь сам».

Сначала ответь сам

Клиент шлёт через nginx запрос с заголовком Connection: close - хочет, чтобы соединение закрыли после ответа. Бэкенд этот заголовок не видит. Проверено: он есть в запросе к nginx, и его нет в том, что дошло до приложения.

nginx его потерял?

Нет, вырезал намеренно. Connection описывает вот это конкретное соединение между клиентом и nginx, а между nginx и бэкендом соединение другое, со своими правилами. Передать такой заголовок дальше означало бы навязать чужому участку чужие условия: клиент попросил закрыть свою связь, а закрылась бы связь nginx с приложением, которую тот держит открытой для сотни других запросов.

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

Посылка едет двумя перевозчиками с перегрузкой на складе.

На коробке два вида надписей. Одни - для получателя: кому, что внутри, осторожно стекло. Их не трогает никто, они едут до конца в том же виде.

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

Заголовки HTTP делятся ровно так же, и делить их приходится не по вкусу, а по смыслу: описывает заголовок соединение или сам запрос.

Механизм: два вида заголовков

клиент nginx приложение соединение 1 соединение 2 сквозные - едут до конца Accept, Cookie, User-Agent, Content-Type, Authorization, Referer, Range, If-None-Match и всё своё: X-Request-Id участковые - снимаются на каждой пересадке Connection, Keep-Alive, Upgrade, Transfer-Encoding, TE, Trailer, Proxy-Authenticate, Proxy-Authorization Host - особый случай: не участковый, но nginx его подменяет по умолчанию
Так их делит стандарт. nginx на деле снимает из запроса четыре - Connection, Keep-Alive, Upgrade и TE, - а Trailer и оба Proxy-* пересылает (замер на 1.31.8). Сквозные относятся ко всему пути запроса, но настройки прокси могут менять их: например, Host.

Правило

Участковый заголовок описывает соединение, сквозной - запрос. Прокси обрабатывает участковые заголовки отдельно для каждого соединения. Сквозные обычно передаются дальше, но правила прокси могут изменить и их, как Host.

Разбор: что бэкенд получил на самом деле

Клиент отправил через nginx шесть заголовков. Вот что дошло до приложения:

↳  выводecho-бэкенд печатает полученный им запрос
GET /echoall HTTP/1.1
Host: app
X-Dup: a
X-Dup: b
Accept-Encoding: gzip

Три вещи разом.

Connection: close исчез - участковый, снят на пересадке, как и положено.

Но снимает nginx не весь список стандарта. Замер на 1.31.8, клиент прислал каждый участковый заголовок плюс Connection: close, X-Foo - этим протокол объявляет участковым и X-Foo:

заголовок от клиента дошёл до бэкенда
Connection, Keep-Alive, TE, Upgrade нет - сняты
Trailer, Proxy-Authorization, Proxy-Authenticate да, как есть
X-Foo, перечисленный в Connection да: nginx этот механизм не применяет

Transfer-Encoding и длину тела nginx на втором участке выставляет сам. Так что если на бэкенд не должен попасть Proxy-Authorization (например, пароль клиента к прокси), его удаляют явно: proxy_set_header Proxy-Authorization "";.

X-My_Header: v1 исчез - но по другой причине: имя с подчёркиванием nginx отбросил ещё при разборе запроса, до всякого проксирования. Разные механизмы, а симптом один - «заголовок не дошёл».

Host стал app - это имя группы серверов из upstream, а не имя сайта, которое просил клиент. Умолчание такое сознательное: nginx подставляет $proxy_host, то есть адрес, на который стучится. Приложение, которое смотрит на Host (а на него смотрят почти все - выбор сайта, генерация ссылок, редиректы), из-за этого начинает считать себя сайтом app. Лечится одной строкой, и она есть в любом рабочем конфиге:

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
proxy_set_header Host $host;

Разбор: где наивное правило подводит

proxy_set_header наследуется «всё или ничего». Директивы не складываются: одна своя в location отменяет весь набор, унаследованный от server и http. Проверено на стенде - в location, где стоит единственная строка proxy_set_header X-Only-This one;, бэкенду ушло:

↳  выводчто дошло до приложения
Host: app
X-Only-This: one

Заботливо поставленный уровнем выше Host: $host исчез, и вернулось умолчание. Это то же правило, что у add_header из раздела про язык конфига, и ловушка та же: добавил один заголовок - потерял все остальные.

Пустое значение удаляет заголовок. proxy_set_header Accept-Encoding ""; - не «передать пустую строку», а «не передавать вовсе». Приём применяют, когда бэкенду не надо отдавать сжатое: пусть отвечает как есть, а сжатием займётся nginx (и сможет положить результат в кеш).

Дубликаты доезжают дубликатами. Два X-Dup ушли бэкенду двумя строками, хотя в переменной nginx они склеены запятой. Значит приложение видит не то же самое, что видит конфиг, и проверку «этот заголовок ровно такой» надо писать с оглядкой.

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

Классика первая - ссылки в письмах, ведущие на http://app/reset-password. Приложение построило их из Host, а Host подставил nginx.

Классика вторая - WebSocket, который «не работает через nginx». Upgrade и Connection - участковые, и nginx их закономерно не пересылает; чтобы соединение поднялось, их надо поставить заново руками. Ровно поэтому в конфигах для WebSocket всегда стоят две одинаковые строки, которые все копируют, не понимая: это не магия, это восстановление участковых заголовков на втором участке.

Классика третья - бэкенд видит всех клиентов с одного адреса. $remote_addr для него - это адрес nginx, и настоящий адрес надо передать отдельно. Как именно и почему X-Forwarded-For нельзя доверять без настройки - в разделе про обратный прокси.

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

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

Второй способ - сравнить два вывода: curl -v к nginx и то, что записало приложение. Сопоставь различия с настройками прокси: удалением участковых заголовков, заменой Host, своими proxy_set_header и правилами приёма нестандартных имён вроде X-My_Header.

Теперь сам

В location /api/ добавили proxy_set_header X-Trace-Id $request_id;. После выкладки приложение начало генерировать ссылки на внутренний адрес, хотя раньше всё было хорошо. Что произошло?

Уровнем выше стоял proxy_set_header Host $host;, и новая директива в location отменила весь унаследованный набор. Host вернулся к умолчанию $proxy_host, приложение сочло себя сайтом с внутренним именем и построило ссылки из него. Лечится повторением всех нужных строк рядом с новой - либо, если наборов много, выносом их в отдельный файл и include в каждом location.

Главное

Проверяй заголовки на каждом участке проксирования. nginx меняет часть заголовков соединения и по умолчанию передаёт бэкенду Host из адреса назначения. Собственный proxy_set_header во вложенном блоке заменяет унаследованный набор целиком, поэтому после такой правки заново проверь сведения о клиенте.

Комментарии

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

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

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