# теория · шаг 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 делятся ровно так же, и делить их приходится не по вкусу, а по смыслу: описывает заголовок соединение или сам запрос.
Механизм: два вида заголовков
Правило
Участковый заголовок описывает соединение, сквозной - запрос. Прокси обрабатывает участковые заголовки отдельно для каждого соединения. Сквозные обычно передаются дальше, но правила прокси могут изменить и их, как Host.
Разбор: что бэкенд получил на самом деле
Клиент отправил через nginx шесть заголовков. Вот что дошло до приложения:
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. Лечится одной строкой, и она есть
в любом рабочем конфиге:
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 во вложенном блоке заменяет унаследованный набор целиком, поэтому после такой правки заново проверь сведения о клиенте.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий