На 31 июля 2026 года строгий аудит проходит 79 из 358 созданных материалов. Остальные 279 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 82 из 358 созданных материалов. Остальные 276 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
Ревизии не содержат полей `date` и `author`: при будущей интеграции сохраняются
дата, автор и slug базового архива.
## Проход 1 — факты и техника
### Проверенные опоры
- [Nginx `ngx_http_proxy_module`](https://nginx.org/en/docs/http/ngx_http_proxy_module.html): `proxy_pass`, явная подстановка `Host`, `proxy_set_header`, а также различие `proxy_connect_timeout`, `proxy_send_timeout` и `proxy_read_timeout`.
- [Nginx `ngx_http_realip_module`](https://nginx.org/en/docs/http/ngx_http_realip_module.html): адрес по header меняется только для источника, который явно назван через `set_real_ip_from`.
- [Nginx `ngx_http_log_module`](https://nginx.org/en/docs/http/ngx_http_log_module.html): `log_format`, `access_log`, `request_time` и роль формата журнала как наблюдаемого артефакта.
- [RFC 7239](https://www.rfc-editor.org/rfc/rfc7239): `Forwarded`, связь со старым семейством `X-Forwarded-*` и ограничение целостности forwarded-данных без доверенной цепочки proxy.
- [RFC 7230](https://www.rfc-editor.org/rfc/rfc7230): исторический HTTP/1.1-контекст апреля 2020 года для маршрутизации и `Host`.
### Что проверено в тексте
1. Reverse proxy описан как граница двух HTTP-hop-ов, а не как «прозрачный провод».
2.`proxy_read_timeout` назван интервалом между последовательными чтениями ответа, а не полной длительностью ответа.
3.`Host`, `X-Forwarded-*` и real IP не объявлены достоверными без источника и договора доверия.
4. 502/504 названы входом в диагностическую ветку, а не самостоятельным диагнозом.
5. Учебная конфигурация не подменяет реальную: `127.0.0.1`, пути, timeout-значения, URL и токены явно помечены заполнителями.
### Границы и запреты
В текст не добавлены service mesh, OpenTelemetry, Kubernetes Ingress или
современные облачные практики как опыт автора 2020 года. Не опубликованы
реальные hostnames, IP-адреса, cookie, токены, secrets или пользовательские
данные. `curl`, access-log и значения времени даны как учебные либо
синтетические примеры; они не выдаются за запуск на стенде.
**Вердикт прохода 1: пройден.** Технические утверждения привязаны к первичным
источникам, а недоказанные эксплуатационные выводы явно ограничены.
## Проход 2 — редактура, голос и структура
### Проверка голоса M3 / апрель 2020
Автор развивает ширину от HTTP и локальной доставки к backend boundary:
<descid="desc">Вертикальная схема показывает порядок расследования: сначала зафиксировать симптом и цену, затем выбрать безопасный маршрут, проверить конфигурацию и заголовки, сопоставить access-log с логом приложения, изменить одну подтверждённую границу и повторить запрос.</desc>
<titleid="title">Граница заголовков между клиентом, Nginx и приложением</title>
<descid="desc">Вертикальная схема показывает, что входные клиентские заголовки не считаются достоверными сами по себе, Nginx формирует согласованный набор полей, а приложение читает только те поля, источник которых определён.</desc>
<titleid="title">Один пользовательский запрос проходит через два HTTP-hop-а</title>
<descid="desc">Вертикальная схема: клиент отправляет внешний запрос Nginx, proxy формирует заголовки и лимиты, затем создаёт отдельный запрос к приложению. Внизу access-log и лог приложения связывают диагностику.</desc>
note:'справочник log_format, access_log и переменной request_time; формат журнала должен быть частью диагностического контракта',
};
constrfc7239={
title:'RFC 7239: Forwarded HTTP Extension',
url:'https://www.rfc-editor.org/rfc/rfc7239',
note:'стандартный заголовок Forwarded, его связь с X-Forwarded-* и ограничение: данные заголовка нельзя считать достоверными без доверенной цепочки proxy',
};
constrfc7230={
title:'RFC 7230: HTTP/1.1 Message Syntax and Routing',
url:'https://www.rfc-editor.org/rfc/rfc7230',
note:'историческая для апреля 2020 года спецификация HTTP/1.1: маршрутизация, Host и границы между получателем и отправителем сообщения',
};
constboundaryConfig=[
'# Учебная конфигурация: имена, адреса и значения не относятся к рабочему контуру.',
excerpt:'Proxy создаёт второе HTTP-соединение и меняет контекст запроса. Собираем малый контракт для заголовков, таймаутов и диагностики вместо правок «вслепую».',
readingMinutes:14,
},
[
paragraph('Симптом обычно выглядит как ошибка приложения: внешний запрос приходит по HTTPS, а код считает его HTTP; все посетители в журнале имеют один адрес; после добавления proxy часть URL вдруг теряет префикс. Цена не в одном неверном заголовке. Неправильная схема ломает редирект или флаг cookie, общий адрес клиента делает бесполезным разбор ошибок, а случайный таймаут превращает медленный ответ в обвинение «бэкенд упал». Если сразу менять middleware или увеличивать лимит, команда лечит последний наблюдаемый слой, а не причину.'),
paragraph('У reverse proxy простая, но важная роль: он принимает внешнее HTTP-соединение и создаёт отдельное соединение к приложению. Между ними меняются адрес peer, схема, часть заголовков, лимиты ожидания и журнал. Поэтому в апреле 2020 года я бы начал не с универсальной конфигурации, а с короткого контракта по hop-ам: что видит клиент, что обязан передать Nginx, что приложение имеет право считать доверенным и какой один маршрут докажет это на изолированном стенде. Ниже приведены учебные значения; они не были применены к browser, staging или production.'),
heading('Прокси — не прозрачный провод'),
paragraph('Пока клиент подключён к Nginx, для Nginx адрес клиента — адрес удалённой стороны этого соединения. Когда Nginx обращается к upstream, приложение видит уже Nginx как непосредственного соседа. Это не ошибка и не повод вручную подменять адрес в каждом контроллере. Это граница, на которой нужно явно договориться о передаваемых данных. Аналогично с TLS: TLS может завершаться перед приложением, а внутренний hop остаётся обычным HTTP. Приложение не может вычислить внешнюю схему из сокета, если proxy не передал ей согласованный признак.'),
paragraph('В этой модели полезно разделить три класса данных. Первый — маршрут: метод, URI и Host, по которым приложение выбирает обработчик и строит ссылки. Второй — происхождение: схема и цепочка адресов, нужные для ограниченного набора решений. Третий — время: когда proxy смог подключиться к upstream, отправить запрос и дождаться следующего байта ответа. Смешать их легко: например, таблица в базе говорит, что URL правильный, а проблема на самом деле в <code>Host</code>, который по умолчанию у proxy не обязан совпадать с внешним host.'),
dataTable(
'Контракт запроса по hop-ам: наблюдение должно иметь владельца',
['Слой','Что он видит','Что может изменить','Что проверять'],
[
['Клиент','внешний URL, статус и ответные заголовки','только свой запрос','метод, путь, ожидаемый статус и безопасный диагностический токен'],
['Nginx','входной запрос и отдельный upstream-hop','Host, X-Forwarded-*, лимиты соединения и чтения','явные proxy_set_header, proxy_pass и формат access-log'],
['Приложение','соединение от proxy и переданные заголовки','свою логику маршрута и журнал','какие заголовки допустимы только от доверенного proxy'],
['Журнал','итог обработки на proxy','ничего не исправляет','статус, request_time и upstream-поля рядом, без секретов и пользовательских данных'],
],
),
paragraph('Эта таблица намеренно не называет конкретную топологию сети. Для одного проекта Nginx стоит прямо перед приложением; для другого перед ним уже есть балансировщик. Во втором случае <code>$remote_addr</code> на Nginx может обозначать предыдущий proxy, а не браузер. Подключать модуль realip имеет смысл только после того, как список доверенных источников определён отдельно. Его <code>set_real_ip_from</code> — это не косметическая настройка, а разрешение заменить адрес по заголовку.'),
heading('Минимальная конфигурация как объект ревью'),
paragraph('В учебной конфигурации ниже upstream намеренно локальный, а <code>server_name</code> не раскрывает ни одного рабочего имени. Она показывает форму договора, а не готовый фрагмент для копирования. <code>Host</code> передаётся явно, потому что у Nginx есть свои значения по умолчанию для заголовков proxied request. <code>X-Forwarded-For</code> накапливается через <code>$proxy_add_x_forwarded_for</code>, а <code>X-Forwarded-Proto</code> фиксирует схему hop-а, который пришёл на Nginx. Приложение должно принимать эти поля только из согласованной границы, а не из любого прямого HTTP-запроса.'),
codeBlock(boundaryConfig),
paragraph('Три таймаута в примере отвечают на разные вопросы. <code>proxy_connect_timeout</code> ограничивает установление соединения с upstream. <code>proxy_send_timeout</code> относится к передаче запроса upstream между последовательными операциями записи. <code>proxy_read_timeout</code> относится к промежутку между чтениями ответа, а не к полной длительности ответа. Поэтому число <code>15s</code> не является «временем работы API»: потоковый ответ может жить дольше, если upstream регулярно отдаёт байты, а тихий запрос может оборваться раньше, если приложение перестало отвечать. Значения здесь учебные, их нельзя переносить в рабочий контур без бюджета ожидания всего маршрута.'),
'Вертикальная схема трёх hop-ов: клиент передаёт запрос Nginx, Nginx фиксирует контракт заголовков и таймаутов, затем создаёт отдельный запрос к приложению; внизу показана точка совместной диагностики по access-log и логу приложения',
'Один пользовательский запрос даёт как минимум два HTTP-hop-а. Ошибку ищем на том hop-е, где меняется нужный сигнал, а не в абстрактном «сервере».',
),
heading('Один безопасный маршрут вместо широкого smoke-теста'),
paragraph('Чтобы проверить контракт, не нужен полный прогон сайта. Нужен маршрут без пользовательских данных и с понятным ответом: например, <code>/health</code> или отдельный endpoint стенда. В запрос добавляется учебный токен, который можно увидеть и в журнале proxy, и в логе приложения, если приложение уже умеет его писать. Токен не становится средством аутентификации и не заменяет request ID; он всего лишь связывает две записи в контролируемом упражнении. Если таких журналов нет, сначала добавляют безопасный формат, а потом запускают проверку.'),
codeBlock(boundaryCurl),
paragraph('После такого запроса нельзя делать вывод «приложение работает за proxy», если пришёл только <code>200</code>. Проверяются четыре вещи: status и ответные заголовки снаружи, запись proxy с тем же токеном, запись приложения с этим же токеном и точное значение схемы или host, которое приложение использовало. Если внешнее соединение TLS, а приложение видит HTTP, это может быть корректно на внутреннем hop-е. Ошибкой становится не сам HTTP, а отсутствие согласованного сигнала, по которому код различает внешнюю схему.'),
heading('Типовые симптомы не лечатся одним заголовком'),
paragraph('Симптом «все адреса одинаковые» имеет минимум две причины. Либо приложение честно пишет peer address и видит Nginx, либо оно доверяет неподтверждённому <code>X-Forwarded-For</code>. Проверка разная: сначала определяем, есть ли прямой доступ к приложению и какой proxy имеет право дописывать заголовок. Действие — закрепить этот маршрут в конфигурации и в приложении, а не заменить адрес на первое значение из любой строки. RFC 7239 отдельно напоминает: forwarded-информация может быть изменена на пути и не становится достоверной без доверенной цепочки.'),
paragraph('Симптом «редирект ведёт на http» тоже не означает, что Nginx сломан. Сначала проверяют, где завершается TLS, какой заголовок proxy передаёт и какое поле реально читает фреймворк. Затем сравнивают один учебный запрос до и после конфигурации. Если приложение использует один заголовок, а proxy передаёт другой, исправляется контракт на границе или адаптер приложения — но не добавляется ещё один независимый способ угадать схему. Так сохраняется возможность объяснить поведение новому человеку по четырём строкам конфигурации.'),
heading('Маршрут внедрения и отката'),
orderedList([
'Записать один симптом в наблюдаемой форме: неверный Host, схема, адрес клиента, 502/504 или неожиданный timeout; назвать его цену для пользователя или разработки.',
'Нарисовать реальные hop-ы без адресов и секретов: кто принимает внешний запрос, где заканчивается TLS и какой процесс является upstream для Nginx.',
'Согласовать для приложения явный набор полей: Host, признак схемы, forwarded-цепочка и один безопасный диагностический токен; отдельно назвать, кто имеет право их выставлять.',
'Собрать итоговую конфигурацию Nginx и проверить её синтаксис в изолированной среде; не менять одновременно route, приложение и значения таймаутов.',
'Выполнить один учебный curl-маршрут на стенде, сопоставить записи proxy и приложения, затем изменить только ту строку конфигурации, которая объясняет расхождение.',
'Сохранить ожидаемый результат и обратный шаг: какие поля и лимиты вернуть, если проверка не подтверждает гипотезу. Без этого увеличение таймаута остаётся необъяснимым обходом.',
]),
heading('Граница должна быть маленькой и явной'),
paragraph('Reverse proxy полезен тем, что отделяет внешний HTTP от процесса приложения. Но это разделение требует контракта: что передаётся, чему доверяют, сколько ждут и где виден результат. В апреле 2020 года для небольшого сервиса достаточно Nginx-конфигурации, одного диагностического маршрута, таблицы сигналов и короткого access-log. Не требуется усложнять схему service mesh, Kubernetes Ingress или распределённой трассировкой, чтобы перестать угадывать причину 502.'),
paragraph('Эта статья не запускала Nginx, curl, browser, staging или production и не использует реальные hostnames, IP-адреса либо секреты. Конфигурация, адрес <code>127.0.0.1</code>, путь журнала и таймауты — только учебные заполнители. Перед применением в проекте нужно отдельно подтвердить топологию, доверенные proxy, таймаут приложения, безопасный диагностический маршрут и правила хранения журналов.'),
excerpt:'Почему приложение видит не тот адрес и схему, а 504 не объясняет причину само по себе: раскладываем proxy на отдельные соединения и проверяем контракт заголовков.',
readingMinutes:15,
},
[
paragraph('Симптом становится дорогим, когда proxy считают невидимой деталью: приложение разрешает небезопасный редирект, потому что видит HTTP; ограничение по адресу клиента работает для всей аудитории как для одного посетителя; ответ внезапно обрывается по 504, и команда увеличивает все таймауты разом. Цена такой реакции — не только задержка. Появляется недоверенный источник данных, длиннее висят соединения, а журнал больше не показывает, на каком участке запрос перестал двигаться.'),
paragraph('Причина в механике: reverse proxy не пересылает тот же самый TCP/HTTP-разговор без изменений. Он завершает входной запрос, принимает решение по своему <code>server</code>/<code>location</code>, а затем создаёт request к upstream. У нового request есть свой peer address, свои default headers, свой момент подключения и свои интервалы чтения. Для автора, который в 2020 году изучает delivery и backend boundary, это не повод строить новую платформу. Достаточно назвать эти состояния и проверить их в маленькой конфигурации, не выдавая учебный запуск за production-факт.'),
heading('Два соединения дают две правды о запросе'),
paragraph('На внешнем hop-е клиент отправляет method, target, Host, заголовки и тело Nginx. Nginx может выбрать location, переписать URI, добавить или удалить заголовок, буферизовать данные и закрыть соединение по своему лимиту. На внутреннем hop-е upstream получает то, что сформировал proxy. Поэтому слово «оригинальный запрос» в обсуждении часто мешает: полезнее каждый раз уточнять, о каком hop-е идёт речь. Внешний URL мог быть HTTPS, а внутренний hop — HTTP; это корректно, если контракт сообщает приложению исходную схему и эта информация пришла от доверенной стороны.'),
paragraph('В Nginx <code>proxy_set_header</code> не является декоративным списком. Документация модуля фиксирует, что по умолчанию proxy задаёт собственные значения для <code>Host</code> и <code>Connection</code>; при явных директивах на уровне location важно увидеть итоговую конфигурацию, а не надеяться на наследование. Именно поэтому ревью конфигурации должно отвечать на прямой вопрос: чему равен Host у upstream, какой заголовок сигнализирует схему и какие headers сознательно не передаются. Иначе приложение может собрать URL с именем upstream либо потерять нужный признак внешнего запроса.'),
dataTable(
'Данные на границе proxy: значение без источника не становится фактом',
['Host','во внешнем запросе; proxy формирует значение для upstream','upstream получает своё имя вместо внешнего маршрута','явно задать policy Host и сверить её с маршрутизацией приложения'],
['X-Forwarded-Proto или Forwarded','proxy после TLS termination или иной согласованной границы','клиент мог прислать поле сам, а приложение приняло его как достоверное','принимать схему только из пути, где известен и ограничен источник proxy'],
['X-Forwarded-For / X-Real-IP','на proxy; возможно, раньше уже был другой proxy','все пользователи выглядят одним peer либо подставленная цепочка считается клиентом','назвать доверенные hop-ы и правило разбора до включения realip или кода приложения'],
['request_time и upstream-поля','в момент записи access-log','504 считают названием причины, а не результатом ожидания','сравнить время proxy с событием приложения и конкретным proxy timeout'],
],
),
paragraph('Стандартный <code>Forwarded</code> из RFC 7239 описывает параметры <code>for</code>, <code>by</code>, <code>host</code> и <code>proto</code>. При этом в старых приложениях широко встречаются <code>X-Forwarded-*</code>, а переход между форматами не происходит сам. RFC подчёркивает более важное ограничение: эти данные можно изменить на пути, в том числе клиентом. Поэтому приложение не должно превращать header в источник прав для каждого прямого соединения. Nginx realip module также требует явно указать доверенные адреса через <code>set_real_ip_from</code>; это хороший сигнал, что доверие — часть конфигурации, а не строка парсинга.'),
heading('Заголовки должны описывать ровно один договор'),
paragraph('Ниже не «рецепт для любого сервера», а учебный минимум. Он показывает, где находятся точки договора. <code>Host</code> передаётся как <code>$host</code>; <code>X-Real-IP</code> показывает непосредственный peer, а <code>X-Forwarded-For</code> расширяет уже имеющуюся цепочку. Если Nginx сам расположен за другим proxy, этот peer может быть адресом предыдущего hop-а. Исправлять это нужно не редактированием X-Forwarded-For в приложении, а отдельной проверкой реальной цепочки и доверенных источников.'),
codeBlock(boundaryConfig),
paragraph('Схема <code>X-Forwarded-Proto $scheme</code> тоже имеет границу. Она показывает схему соединения, пришедшего именно на этот Nginx. Если TLS завершился раньше, <code>$scheme</code> может быть <code>http</code>, хотя пользователь открыл HTTPS. В таком случае нельзя просто жёстко записать <code>https</code>: сначала фиксируют, где происходит termination, какой proxy законно сообщает исходную схему и как этот proxy защищён от прямого обхода. Результатом является один договор, а не несколько конкурирующих headers, из которых код выбирает удобный.'),
'Вертикальная схема границы заголовков: внешний запрос несёт недоверенные клиентские поля, Nginx формирует договор Host и X-Forwarded-*, приложение читает только согласованные поля от известного proxy, а отдельный access-log связывает оба hop-а',
'Header становится пригодным для решений только вместе с источником. Строка без описанной доверенной границы не доказывает ни схему, ни адрес клиента.',
),
heading('Три таймаута отвечают на разные паузы'),
paragraph('Число в конфигурации не равно времени ответа API. <code>proxy_connect_timeout</code> ограничивает установление соединения с proxied server. <code>proxy_send_timeout</code> применим между последовательными операциями записи запроса upstream. <code>proxy_read_timeout</code> применяется между последовательными операциями чтения ответа. Последний пункт особенно важен в диагностике: длинный ответ, который регулярно отдаёт данные, и зависший upstream без следующего байта выглядят для него по-разному. Называть оба случая «медленным запросом» значит потерять полезную развилку.'),
paragraph('На практике я сначала рисую бюджет без выдуманных чисел: сколько имеет право ждать клиент, сколько proxy готов ждать подключения, сколько — первый или следующий байт, и где у приложения собственное ограничение. Затем выбираю один класс запроса. Для обычного JSON endpoint не нужен бесконечный read timeout. Для streaming endpoint нельзя применять тот же лимит без понимания пауз в протоколе. Если timeout меняется, рядом фиксируется ожидаемое изменение в access-log и способ вернуть старое значение. Это делает конфигурацию предметом ревью, а не накоплением чисел из чужих статей.'),
dataTable(
'Таймаутный разбор: что наблюдать до изменения числа',
['Наблюдение','Не делать вывод','Проверка','Следующее действие'],
[
['Не удалось подключиться к upstream','«приложение медленно отвечает»','отдельно сверить proxy_connect_timeout и доступность именно upstream-hop-а','исследовать адрес, порт, процесс или лимит подключения; не увеличивать read timeout'],
['Подключение есть, но ответ не даёт следующего байта','«нужно увеличить всё»','сопоставить proxy_read_timeout, access-log и приложение на одном токене','найти ожидание в приложении или осознанно изменить лимит для данного типа endpoint'],
['Клиент разрывает запрос раньше proxy','«Nginx вернул ошибку сам»','смотреть внешний timeout клиента отдельно от request_time proxy','согласовать бюджет между клиентом, proxy и приложением'],
['У streaming endpoint есть регулярные части ответа','«долгий ответ всегда ошибка»','проверить паузу между частями и назначение endpoint','выделить отдельную policy, а не переносить общий JSON-лимит'],
],
),
heading('Журнал связывает гипотезу с hop-ом'),
paragraph('Access-log полезен, когда в одной строке есть путь, итоговый status, полное <code>request_time</code> и upstream-поля. Nginx log module документирует <code>log_format</code> и <code>$request_time</code>; proxy module даёт данные о выбранном upstream и времени этого взаимодействия. Такой формат не раскрывает cookie, authorization header или реальный адрес пользователя. Он нужен, чтобы сделать различимыми хотя бы две гипотезы: proxy не подключился к upstream или подключился, но не получил заголовки ответа до своего лимита.'),
codeBlock(headerLogConfig),
paragraph('Сам лог не говорит, какой именно запрос в приложении был дорогим. Для этого в учебном упражнении добавляют безопасный токен и сверяют его с логом приложения, если такой лог уже существует. Не стоит добавлять в access-log полный query string, тело или заголовок авторизации ради удобства расследования. В 2020 году для небольшой команды достаточно минимального формата и дисциплины: одно поле добавляют только если заранее понятно, какую развилку оно поможет проверить.'),
heading('Последовательность разбора механизма'),
orderedList([
'Назвать внешний симптом и стоимость: неверная схема, адрес, Host, 502/504 или превышение ожидания; не смешивать эти варианты в одну жалобу.',
'Нарисовать два соединения и отметить TLS termination, Nginx location и конкретный upstream; не публиковать реальные имена, адреса и секреты.',
'Проверить итоговую конфигурацию proxy_set_header: Host, схема, цепочка адресов, Connection и policy для недоверенных прямых запросов.',
'Разделить connect, send и read timeout, затем сопоставить каждый лимит с типом endpoint и наблюдаемым промежутком ожидания.',
'Добавить или проверить минимальный log_format без чувствительных данных и пройти один контролируемый запрос на изолированном стенде.',
'Изменить одну причину, повторить тот же маршрут и записать ограничение: какие hop-ы или proxy ещё не входят в договор.',
]),
heading('Что эта модель не обещает'),
paragraph('Она не делает header безопасным сама по себе, не превращает status 504 в диагноз и не выбирает «правильные» таймауты без нагрузки и сценария. Она лишь отделяет факты: где запрос был принят, где был сформирован новый request, что proxy передал дальше и между какими операциями истекло время. Для начинающего движения в delivery это уже заметный шаг: ошибка перестаёт быть свойством «сервера вообще» и получает конкретную границу, конфигурацию и проверку.'),
paragraph('Здесь не запускались Nginx, curl, browser, CI, staging или production. Адрес <code>127.0.0.1</code>, путь журнала и таймауты принадлежат учебному примеру, а не реальному контуру; никаких hostnames, секретов и фактических IP не публикуется. Перед использованием нужны отдельные проверки топологии, доверенных proxy, версии Nginx, поведения фреймворка при forwarded headers и требований к хранению диагностических журналов.'),
excerpt:'502 или 504 — это начало расследования, а не имя причины. Собираем безопасную таблицу сигналов, учебный access-log и маршрут проверки одного proxy-hop-а.',
readingMinutes:14,
},
[
paragraph('Симптомы proxy часто приходят пачкой: клиент получает 504, приложение пишет HTTP вместо HTTPS, а журнал видит один и тот же адрес для каждого пользователя. Цена поспешного исправления заметна быстро: таймаут увеличивают для всех endpoint-ов, в код добавляют исключение для «настоящей схемы», а access-log остаётся без полей, которые могли бы отделить подключение от ожидания ответа. Через неделю тот же сбой возвращается, но уже с другим status и без возможности сравнить два случая.'),
paragraph('Полезный разбор начинается с ограничения. Здесь нет production-инцидента и нет реального access-log: ниже синтетический сценарий с учебными полями. Его задача — научить отличать сигнал от вывода. Reverse proxy формирует отдельный upstream-hop, поэтому один status не говорит, где именно потеряно время или кто изменил заголовок. В апреле 2020 года достаточно собрать путь одного запроса, безопасный формат лога и короткую таблицу «симптом → причина → проверка → действие», не приписывая себе запуск на живом контуре.'),
heading('Начинаем с одного маршрута и одного вопроса'),
paragraph('Выбираю маршрут, на котором нет персональных данных и который допускает повтор: <code>/health</code>, техническая страница или специальный стендовый endpoint. Вопрос тоже один. Например: «получил ли Nginx заголовки ответа upstream до истечения своего read timeout?» Это лучше, чем вопрос «почему всё медленно». Для ответа нужны совпадающие признаки: путь и безопасный токен в входном запросе, итоговый status и время в access-log, плюс запись приложения, если она уже умеет логировать этот токен. Если последней записи нет, это результат наблюдения, а не повод придумать её содержание.'),
paragraph('Следующий вопрос — о границе доверия: «кто установил X-Forwarded-Proto и X-Forwarded-For?» Приложение может видеть адрес Nginx совершенно корректно. Проблемой это становится, когда приложение использует этот peer как пользовательский адрес или принимает forwarded-header от прямого внешнего соединения. Nginx realip module требует отметить trusted sources, а RFC 7239 предупреждает, что forwarded-информация может быть изменена на пути. Поэтому сначала рисуется топология, затем определяется доверенный proxy, и только после этого меняется конфигурация или middleware.'),
dataTable(
'Карта диагностики: status — вход в ветку, а не готовый диагноз',
['504 на одном endpoint','ожидание proxy между операциями чтения upstream','сопоставить proxy_read_timeout, request_time, upstream_header_time и лог приложения на одном токене','исправить задержку upstream или отдельно пересмотреть лимит именно этого endpoint'],
['502 после смены proxy_pass','маршрут до upstream, URI или ответ upstream','прочитать итоговый location/proxy_pass и status upstream без изменения таймаутов','вернуть один неверный маршрут либо исправить конфигурацию; не лечить 502 read timeout'],
['Приложение строит http-ссылку','TLS termination и договор о схеме','сверить внешний маршрут, $scheme на Nginx и поле, которое читает фреймворк','зафиксировать единственный доверенный forwarded-сигнал'],
['Все пользователи имеют адрес proxy','peer address и realip boundary','выяснить, существует ли прямой доступ к приложению и какие hop-ы доверены','не парсить первый X-Forwarded-For; задать список доверенных источников отдельно'],
],
),
paragraph('В таблице нет строки «перезапустить всё». Перезапуск может совпасть с исчезновением эффекта, но не доказать причину. Если конфигурация изменилась одновременно с приложением, сначала возвращают понятный контроль: один location, один upstream, один запрос. Если симптом связан с timeout, нельзя одновременно увеличивать <code>proxy_read_timeout</code>, менять пул соединений и добавлять retry. Иначе следующая запись в журнале не ответит, какая из трёх правок что изменила.'),
heading('Учебный access-log должен сохранять развилку'),
paragraph('Nginx пишет access-log в формате, который задаётся через <code>log_format</code>. Для proxy-разбора хватает небольшой строки: метод и URI, итоговый status, <code>$request_time</code>, выбранный upstream, его status и времена подключения, заголовков и ответа. Значения upstream-полей иногда остаются пустыми; это тоже сигнал о том, что нужно проверить конкретную ветку, а не маскировать пустоту нулём. Формат не должен писать authorization, cookie, тело или реальный IP пользователя: диагностика не оправдывает сбор лишних данных.'),
codeBlock(headerLogConfig),
paragraph('После настройки формата удобнее прочитать одну синтетическую строку, чем десятки настоящих. В примере ниже <code>connect</code> маленький, а <code>header</code> отсутствует до 3.001 секунды. Это поддерживает гипотезу, что Nginx быстро начал upstream-hop, но не получил заголовки ответа до лимита. Это не доказывает, что виновата база, GC, внешняя зависимость или сам framework. Следующий шаг — посмотреть конфигурацию и безопасную запись приложения для того же токена, а не объявить причину по одной цифре.'),
codeBlock(syntheticLog),
paragraph('Та же дисциплина применима к 502. Status 502 сообщает, что proxy не смог отдать клиенту корректный upstream-ответ в данной конфигурации, но не заменяет сравнение location, <code>proxy_pass</code>, URL и лога upstream. Если рядом нет <code>upstream_status</code>, это может сузить ветку, но не выбирает её автоматически. Правильная запись расследования звучит короче и честнее: «на учебном маршруте proxy не получил заголовки upstream до указанного лимита; следующий эксперимент — сверить endpoint и лог процесса». В ней нет ни выдуманного запуска, ни обещания, что увеличение таймаута решит всё.'),
'Вертикальная схема расследования reverse proxy: зафиксировать один симптом, выбрать один безопасный маршрут, проверить контракт headers и timeout, сопоставить proxy access-log с логом приложения, затем изменить только подтверждённую границу и повторить маршрут',
'Диагностика не начинает с значения timeout. Сначала определяется hop и наблюдаемый сигнал, затем меняется одна причина.',
),
heading('Неверная схема и адрес — отдельные ветки'),
paragraph('Схема и адрес часто попадают в один «proxy bug», хотя проверяются по-разному. Для схемы важно место TLS termination и заголовок, который приложение реально читает. На внутреннем HTTP-hop-е <code>$scheme</code> может быть <code>http</code>, даже если внешний пользователь пришёл по HTTPS. Для адреса важно, кто является непосредственным peer и кто имеет право заменить его данными из header. <code>set_real_ip_from</code> описывает доверенную сторону; отсутствие такого знания нельзя компенсировать тем, что приложение возьмёт первый элемент <code>X-Forwarded-For</code>.'),
paragraph('Если проект использует стандартный <code>Forwarded</code>, правила его сохранения и расширения должны быть описаны вместе с proxy. Если используется <code>X-Forwarded-*</code>, это не делает решение неверным, но добавляет обязанность назвать формат и источник. Нельзя склеивать значения из <code>Via</code>, <code>Forwarded</code> и X-заголовков так, будто они всегда описывают одну упорядоченную цепочку. RFC 7239 прямо отделяет эти форматы и не обещает, что каждый proxy обновляет их одинаково. Для небольшого сервиса практичнее выбрать один договор и проверить его на одном маршруте.'),
heading('Порядок проверки без широкого перезапуска'),
orderedList([
'Зафиксировать один симптом, один URL-шаблон без чувствительных данных и один ожидаемый результат; назвать стоимость, если он повторится.',
'Отметить на схеме внешний клиентский hop, Nginx и upstream; отдельно указать, где завершается TLS и есть ли proxy перед Nginx.',
'Проверить итоговые директивы proxy_pass, proxy_set_header и timeout для конкретного location, а не фрагмент из другого include-файла.',
'Собрать безопасный access-log с request_time и upstream-полями, затем выполнить один учебный запрос на изолированном стенде с диагностическим токеном.',
'Сопоставить status и время proxy с записью приложения только для этого токена; если записи приложения нет, зафиксировать неизвестное вместо домысла.',
'Изменить одну подтверждённую границу, повторить тот же запрос и записать критерий отката. Если результат не изменился, вернуться к следующей строке таблицы, а не расширять действие.',
]),
heading('Пример проверки заголовков без реального endpoint'),
paragraph('Ниже показана команда с подстановками. Её назначение — сделать будущий учебный запрос повторяемым: оператор заменяет только URL и Host стенда, а не копирует скрытые cookie или рабочую авторизацию. До запуска нужно договориться, какой ответ безопасно проверять и где появится диагностический токен. Команда не была выполнена и не подтверждает доступность какого-либо адреса.'),
codeBlock(boundaryCurl),
paragraph('После запуска стендового варианта смотрят не только на <code>200</code>. Для схемы проверяют, что приложение использовало согласованный forwarded-signal, а не peer socket. Для адреса проверяют путь доверия от входного proxy до кода. Для timeout сравнивают нужный интервал с данными proxy и лога приложения. Если обработчик отдаёт streaming response, отдельной проверкой фиксируют интервал между частями: <code>proxy_read_timeout</code> измеряет паузу между чтениями, а не полную длительность передачи. Это ограничение меняет постановку задачи и должно остаться рядом с выбранным значением.'),
heading('Что остаётся после исправления'),
paragraph('Хороший разбор оставляет не «правильный timeout», а повторяемый маршрут: схема hop-ов, список заголовков с источником, минимальный log_format и один безопасный запрос. В следующий раз это позволяет ответить быстрее: проблема в соединении с upstream, в паузе ответа, в контракте схемы или в недоверенном адресе. Так автор развивает ширину от frontend HTTP к delivery boundary, но не притворяется владельцем большой распределённой платформы. Для этого уровня достаточно видеть свою границу и не скрывать неизвестное.'),
paragraph('Реальные Nginx, curl, browser, CI, staging и production в этой статье не запускались. Синтетическая строка лога, значения времени, адрес <code>127.0.0.1</code>, URL-подстановки и путь файла приведены только для обучения; hostnames, секреты, реальные IP и пользовательские данные не публикуются. Перед применением нужен отдельный прогон в согласованном стенде, проверка версии Nginx, владельца конфигурации, доверенных proxy и политики журналирования.'),
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.