{ "index": 36, "slug": "editorial-2027-01-practice-debugging-decade", "title": "502, пустой экран и 403: диагностика по различающим сигналам", "excerpt": "Практический маршрут от web-симптома к проверяемой причине: как сохранить конверт запроса, связать границы и отличить исправление от удачного повтора.", "contentHtml": "
Пользователь открывает страницу и получает 502. Или видит пустой экран после ответа 200. Или форма возвращает 403, хотя доступ должен быть разрешён. Команда быстро называет причину: «упал сервис», «сломался фронтенд», «протух токен». Если первая версия неверна, инженер меняет не тот слой, стирает исходный сигнал и получает новый симптом. Для пользователя это недоступная операция. Для команды — лишний релиз и повторный отказ.
Диагностика должна идти от наблюдаемого эффекта к различающему сигналу. Сначала сохраните конверт запроса. Затем назовите две причины, совместимые с фактами. Для каждой запишите проверку, которая может её опровергнуть. Только после этого меняйте конфигурацию или код. Такой порядок превращает «похоже на» в последовательность, которую может повторить другой инженер.
HTTP-статус описывает ответ на одной границе. RFC 9110 определяет 502 как ответ gateway или proxy, который получил недействительный ответ от входного сервера, и 504 как ситуацию, в которой gateway не получил своевременный ответ от upstream. Это полезные факты о границе, но не диагноз: за ними могут стоять несовместимый формат, закрытое соединение, неверный маршрут или исчерпанный таймаут.
403 тоже не равен фразе «у пользователя нет доступа». По RFC 9110 сервер понял запрос, но отказался его выполнить. Причина может находиться в claims, scope, ресурсе или версии политики. Если отправитель получил валидные credentials, но они недостаточны для доступа, 403 соответствует этому результату; выяснить, почему credentials оказались недостаточны, можно только по контексту решения.
Пустой экран требует отдельного разбиения. Браузер мог получить пустой HTML. JavaScript мог завершиться с ошибкой до рендера. API мог вернуть пустой массив по корректному условию. Компонент мог скрыть ошибку и оставить контейнер без содержимого. Все случаи похожи на скриншоте, но различаются телом ответа, сетевыми событиями, ошибками консоли и фактически построенным DOM.
Сохраните один пример сбоя и один успешный пример с тем же маршрутом до изменения системы. Минимальный конверт содержит метод, путь, статус, время с часовым поясом, длительность, размер ответа, content-type, request-id или traceparent и границу наблюдения. Для браузерного симптома добавьте URL документа, статус API, ошибку консоли и результат проверки DOM.
Не записывайте в такой конверт cookies, токены, полный пользовательский ввод и другие секреты. Для корреляции достаточно технического идентификатора и безопасной части контекста. Поле boundary должно отвечать на вопрос «где это наблюдали»: browser, gateway или application. Без этой пометки одинаковый статус легко принять за одно и то же событие.
{
'request_id': 'req-7f31',
'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
'method': 'GET',
'route': '/reports',
'status': 502,
'duration_ms': 184,
'response_bytes': 11,
'content_type': 'text/plain',
'boundary': 'gateway'
}Это не универсальный формат логирования. Он задаёт точку сравнения для одного маршрута и одной границы. Названия полей, формат времени и срок хранения принадлежат вашему проекту. Требование к результату проще: gateway и приложение должны уметь найти относящиеся к одному запросу записи, а разные операции не должны случайно получать один и тот же идентификатор.
Полезная гипотеза имеет форму «если X, то при проверке Y увидим Z». В ней есть ожидаемый признак и результат, который заставит отказаться от объяснения. Например: если gateway не получил ответ приложения, в его access log будет 502, а в журнале приложения не появится событие с тем же идентификатором. Если приложение сформировало ошибочный ответ, записи будут на обеих границах, а статусы, размеры или длительности могут различаться.
Идентификатор запроса связывает записи, но не доказывает причинность. Заголовок мог потеряться на прокси, трасса могла быть отобрана sampling-механизмом, а часы узлов могут быть настроены с разным смещением. При отсутствии ключа вывод звучит так: «граница не наблюдаема», а не «виновата ближайшая строка журнала». Это отрицательное знание подсказывает, какой сигнал добавить перед следующим повтором.
Разделяйте три типа телеметрии. Лог — запись отдельного события и его контекста. Span — отрезок операции внутри трассы, с родителем и длительностью. Метрика — измерение, собранное во времени для множества операций. Трасса показывает путь конкретного запроса, но не гарантирует, что каждый участок был записан. Метрика показывает масштаб проблемы, но не выбирает виновный запрос. Лог даёт детали, но без общего ключа плохо связывается с соседними слоями.
Ниже — чистая функция, которая выбирает следующий шаг из зафиксированных наблюдений. Она не делает сетевых запросов и не называет первопричину. Входные события известны, результат каждой ветки можно сравнить с ожидаемым, а отсутствие сигнала остаётся отдельным исходом.
function nextCheck(event) { const status = Number(event.status); const hasTrace = typeof event.traceparent === 'string' && event.traceparent.length > 0; const appSeen = event.application_seen === true; const domReady = event.dom_ready === true; if ((status === 502 || status === 504) && !hasTrace) return 'добавить корреляцию на границе gateway'; if ((status === 502 || status === 504) && hasTrace && !appSeen) return 'проверить маршрут gateway, DNS и соединение до upstream'; if ((status === 502 || status === 504) && hasTrace && appSeen) return 'сравнить ответ приложения, длительности и таймауты границ'; if (status === 200 && !domReady) return 'сравнить HTML, ошибки консоли, API-response и DOM'; if (status === 403) return 'сверить контекст авторизации и решение политики доступа'; return 'зафиксировать следующий сигнал для этой границы'; } const cases = [{ status: 502, application_seen: false }, { status: 502, traceparent: '00-abc-123-01', application_seen: true }, { status: 200, dom_ready: false }, { status: 403, application_seen: true }]; console.log(cases.map(nextCheck));Первый объект показывает, почему отсутствие идентификатора — самостоятельный дефект наблюдаемости. Второй переводит проверку к сравнению ответа и таймаутов, но не доказывает неисправность приложения. Третий разделяет «HTTP 200» и «страница готова». Четвёртый отправляет расследование к контексту доступа, а не к случайному перезапуску. Функция полезна как тест маршрута диагностики; её нельзя использовать как готовое правило прокси или авторизации.
| Наблюдение | Две рабочие гипотезы | Различающий сигнал | Безопасное действие |
|---|---|---|---|
| 502 на gateway | Некорректный ответ upstream или отказ до приложения | traceparent/request-id на gateway и запись приложения в том же контексте | Сначала найти границу разрыва; затем менять маршрут или код |
| 504 через близкий интервал | Истёк таймаут gateway или зависла зависимость | Длительности span-ов и значения таймаутов на каждой границе | Сопоставить бюджет времени; не увеличивать таймаут без измерения |
| 200 и пустой экран | Пустые данные или ошибка рендера | Тело HTML/API, console error и фактический DOM | Повторить с теми же входами и проверить отрицательный путь |
| 403 для ожидаемого пользователя | Неверный контекст или отказ политики | Токен, claims, scope, ресурс и версия правила | Изменить конкретное условие; не ослаблять всю политику |
| Событие есть только на одной границе | Потерян заголовок или отсутствует запись | Формат переноса идентификатора и временное окно | Считать цепочку несвязанной и добавить сигнал |
Один успешный запрос не закрывает расследование. Сравните минимум четыре поля: статус, длительность, размер ответа и наличие связанной записи на следующей границе. Для пользовательской страницы добавьте DOM и console error. Успешный результат должен повторяться на том же входе, а отрицательный сценарий должен по-прежнему завершаться ожидаемым отказом.
Если исправили маршрут, отдельно проверьте старый и новый upstream. Если изменили таймаут, измерьте время до отказа и нагрузку на зависимость. Если добавили retry, проверьте число попыток и суммарную стоимость запроса: повтор может увеличить нагрузку и скрыть первичный сбой. Если изменили политику доступа, проверьте соседние роли, чтобы разрешение одному контексту не стало разрешением всем.
Этот метод работает только с доступными наблюдениями. Он не восстанавливает событие, которое система не записала, и не превращает близкое время в доказательство связи. Если gateway удаляет traceparent, нужно исправлять перенос или добавлять собственный безопасный request-id. Если sampling отбросил span, отсутствие span не означает отсутствие операции.
Семантика статуса зависит от границы. Согласно RFC 9110, 502 относится к недействительному ответу от входного сервера, к которому обращался gateway, а 504 — к отсутствию своевременного ответа. Конкретная реализация может вернуть 502 из-за формата ответа, соединения или настройки маршрута. Поэтому статус задаёт направление поиска, но не выбирает единственную причину.
Учебная функция намеренно грубая: она не учитывает CDN, кеш, редиректы, несколько upstream, фоновые очереди, разные протоколы и локальные правила безопасности. В рабочем окружении нельзя копировать её ветки в firewall, балансировщик или middleware без отдельной проверки контракта. Не помещайте в корреляционные поля токены, персональные данные и тело запроса.
Расследование можно передать другому инженеру, когда он получает исходный конверт, видит границу отказа и может повторить различающую проверку без устного контекста. Исправление готово, если тот же сценарий проходит по ожидаемому пути, отрицательный вариант получает правильный отказ, а связанные записи сохраняются на всех заявленных границах.
Если после изменения «стало зелёным», но нет объясняющего сигнала, честный итог — «симптом исчез, причина не доказана». В таком случае следующий шаг — улучшить наблюдаемость, повторить сценарий и только потом закреплять решение.
traceparent и tracestate. Наличие заголовка помогает связать операции, но не гарантирует запись полной трассы.