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-), которые описывают текущий участок пути. Прокси их не пересылает дальше - именно поэтому 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 приложение соединение 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 шесть заголовков. Вот что дошло до приложения:

↳  выводecho-бэкенд печатает полученный им запрос
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. Лечится одной строкой, и она есть в любом рабочем конфиге:

/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 и то, что записало приложение. Разница между ними и есть работа прокси, и она обязана состоять только из участковых заголовков плюс того, что ты поставил сам.

Теперь сам

В 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 наследуются «всё или ничего»: одна своя отменяет все унаследованные. Пустое значение удаляет заголовок. Имя с подчёркиванием теряется ещё раньше, на разборе запроса.

Комментарии

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

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

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