# теория · шаг 5 из 7
Заголовки, которые прокси обязан менять
Коротко
Прокси - не труба. Часть заголовков он обязан снять и написать заново, иначе сломает протокол, а часть обязан пронести нетронутыми, иначе сломает приложение.
- Заголовки делятся на сквозные (для конечного получателя) и участковыеУчастковый заголовокВосемь заголовков (Connection, Keep-Alive, Upgrade, Transfer-Encoding, TE, Trailer и два Proxy-), которые описывают текущий участок пути. Прокси их не пересылает дальше - именно поэтому WebSocket через nginx требует поставить Upgrade и Connection заново., hop-by-hop (для соседа по цепочке). Вторые прокси не пересылает никогда.
- Участковых немного и список закрыт:
Connection,Keep-Alive,Upgrade,Transfer-Encoding,TE,Trailer,Proxy-Authenticate,Proxy-Authorization. Hostбэкенду по умолчанию уходит не тот, что прислал клиент: nginx ставит туда имя upstream.proxy_set_headerработает по правилу «всё или ничего»: одна своя директива вlocationотменяет все унаследованные.- Пустое значение (
proxy_set_header X "";) заголовок удаляет.
Если разница сквозных и участковых заголовков и правило «всё или ничего» знакомы - листай до «Теперь сам».
Сначала ответь сам
Клиент шлёт через nginx запрос с заголовком Connection: close - хочет, чтобы
соединение закрыли после ответа. Бэкенд этот заголовок не видит. Проверено:
он есть в запросе к nginx, и его нет в том, что дошло до приложения.
nginx его потерял?
Нет, вырезал намеренно. Connection описывает вот это конкретное соединение
между клиентом и nginx, а между nginx и бэкендом соединение другое, со своими
правилами. Передать такой заголовок дальше означало бы навязать чужому участку
чужие условия: клиент попросил закрыть свою связь, а закрылась бы связь nginx с
приложением, которую тот держит открытой для сотни других запросов.
На что это похоже
Посылка едет двумя перевозчиками с перегрузкой на складе.
На коробке два вида надписей. Одни - для получателя: кому, что внутри, осторожно стекло. Их не трогает никто, они едут до конца в том же виде.
Другие - для текущего участка: номер рейса, имя водителя, «выгрузить в третьи ворота». На складе такие наклейки снимают и клеят новые - для второй машины. Оставить старые было бы не бережностью, а ошибкой: второй водитель поехал бы в ворота чужого склада.
Заголовки HTTP делятся ровно так же, и делить их приходится не по вкусу, а по закрытому списку.
Механизм: два вида заголовков
Правило
Участковый заголовок описывает соединение, сквозной - запрос. Прокси переписывает первые и не трогает вторые.
Разбор: что бэкенд получил на самом деле
Клиент отправил через nginx шесть заголовков. Вот что дошло до приложения:
GET /echoall HTTP/1.1
Host: app
X-Dup: a
X-Dup: b
Accept-Encoding: gzip
Три вещи разом.
Connection: close исчез - участковый, снят на пересадке, как и положено.
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 и то, что записало
приложение. Разница между ними и есть работа прокси, и она обязана состоять
только из участковых заголовков плюс того, что ты поставил сам.
Теперь сам
В location /api/ добавили proxy_set_header X-Trace-Id $request_id;. После
выкладки приложение начало генерировать ссылки на внутренний адрес, хотя раньше
всё было хорошо. Что произошло?
Уровнем выше стоял proxy_set_header Host $host;, и новая директива в location
отменила весь унаследованный набор. Host вернулся к умолчанию $proxy_host,
приложение сочло себя сайтом с внутренним именем и построило ссылки из него.
Лечится повторением всех нужных строк рядом с новой - либо, если наборов много,
выносом их в отдельный файл и include в каждом location.
Главное
Заголовки бывают сквозные (для приложения) и участковые (для соединения);
участковых восемь, список закрыт, и прокси их не пересылает - отсюда две
обязательные строки в любом конфиге для WebSocket. Host бэкенду по умолчанию
уходит как $proxy_host, то есть имя upstream, а не сайта - почти всегда нужен
proxy_set_header Host $host. Директивы proxy_set_header наследуются «всё или
ничего»: одна своя отменяет все унаследованные. Пустое значение удаляет
заголовок. Имя с подчёркиванием теряется ещё раньше, на разборе запроса.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий