{ "index": 36, "slug": "editorial-2027-01-practice-debugging-decade", "title": "502, пустой экран, отказ: как проверить причину по сигналам", "excerpt": "Практический маршрут от наблюдаемого web-симптома к проверяемой гипотезе: что сохранить, где искать разрыв и когда исправление действительно готово.", "contentHtml": "
Пользователь открывает страницу, а получает 502. Или видит пустой экран после ответа 200. Или форма отвечает 403, хотя доступ должен быть разрешён. В каждом случае команда быстро называет причину: «упал сервис», «сломался фронтенд», «протух токен». Если первая версия неверна, инженер меняет не тот слой, стирает исходный сигнал и тратит часы на новый симптом. Для пользователя это недоступная операция. Для команды — лишний релиз, повторный инцидент и решение, которое трудно откатить.
Тезис этой статьи простой: диагностика должна идти от наблюдаемого эффекта к различающему сигналу, а не от любимого инструмента к предполагаемому виновнику. Сначала сохраните конверт запроса. Затем назовите две правдоподобные причины. Для каждой запишите проверку, которая может её опровергнуть. Только после этого меняйте конфигурацию или код.
HTTP-статус описывает ответ на одной границе. Статус 502 означает, что шлюз или прокси получил недействительный ответ от вышестоящего сервера. Он не сообщает, разорвалось ли соединение, истёк ли таймаут, не совпал ли маршрут или приложение вернуло неожиданные данные. Статус 504 говорит о таймауте шлюза, но тоже не указывает, на каком участке закончился бюджет времени. Поэтому запись «получили 502» — факт, а не диагноз.
Пустой экран требует такой же дисциплины. Браузер мог получить пустой HTML. JavaScript мог не выполнить рендер. API мог вернуть пустой массив по корректному условию. Компонент мог скрыть ошибку и вывести контейнер без содержимого. Все четыре случая выглядят похоже на скриншоте. Различают их тело ответа, ошибки консоли, сетевые события и фактически построенный DOM.
Сохраните исходный сигнал до повтора и до исправления. Минимальный конверт содержит метод, путь, статус, время, длительность, размер ответа, content-type, идентификатор запроса и границу наблюдения. Для браузерного симптома добавьте URL документа, статус API, ошибку консоли и результат проверки DOM. Эти поля не доказывают причину. Они ограничивают поиск и показывают, какого сигнала пока не хватает.
Формулируйте гипотезу в условной форме: «если причина X, то при проверке Y увидим Z». Такая запись заранее допускает отрицательный результат. Например: если gateway не получил ответ приложения, в access log будет 502, а в application log не будет события с тем же идентификатором. Если приложение вернуло ошибку, обе записи появятся в одном временном окне, а статусы и длительности будут различаться.
Идентификатор запроса связывает записи, но не доказывает причинность. Он может потеряться на границе, повториться из-за ошибки интеграции или не попасть в sampled trace. Совпадение времени тоже не является связью: параллельные запросы имеют похожие отметки, а часы узлов могут расходиться. Если ключа нет, это результат проверки — «цепочка не связана», — а не разрешение взять ближайшую запись.
Для распределённого маршрута полезно различать три вида данных. Log показывает сообщение и локальное состояние процесса. Span показывает границу операции, родителя и длительность. Metric показывает агрегат по множеству запросов. Trace ID помогает найти общий контекст. Ни один из этих сигналов в одиночку не отвечает на все вопросы. Длинный span не обязательно является причиной задержки. Ошибка в метрике не доказывает ошибку конкретного запроса.
Ниже — локальный учебный пример. Он не обращается к сети, не читает production-логи и не утверждает, что найден источник ошибки. Функция только выбирает следующий сигнал по форме ответа. Автоматизация может упорядочить проверку, но не может выдать доказательство из отсутствующих данных.
function classifyWebSymptom(input) {
const status = Number(input?.status);
const body = String(input?.body ?? '');
const headers = Object.fromEntries(Object.entries(input?.headers ?? {}).map(([key, value]) => [key.toLowerCase(), String(value)]));
if (status === 502 || status === 504) return headers.traceparent || headers['x-request-id'] ? 'связать gateway и upstream по идентификатору' : 'включить идентификатор на границе';
if (status === 401 || status === 403) return 'сверить аутентификацию, авторизацию и политику доступа';
if (status === 200 && /empty|blank|undefined/i.test(body)) return 'сравнить тело ответа API с фактическим DOM';
return 'сохранить метод, путь, статус, размер и время';
}
console.log(classifyWebSymptom({ status: 502, headers: { traceparent: '00-abc-123-01' }, body: 'Bad Gateway' }));
// связать gateway и upstream по идентификаторуВход с 502 и traceparent ведёт к сопоставлению записей на двух границах. Вход с 502 без идентификатора ведёт к исправлению наблюдаемости, а не к перезапуску приложения. Вход с 200 и пустым содержимым ведёт к сравнению ответа, ошибок рендера и DOM. Вход 403 ведёт к проверке схемы аутентификации и решения авторизации. Эти маршруты не заменяют расследование. Они не дают функции права выбрать базу данных, прокси или браузер виновником.
| Симптом | Возможная причина | Проверка | Действие |
|---|---|---|---|
| 502 на границе | gateway не получил корректный ответ upstream | Сопоставить request-id или traceparent; проверить запись приложения в том же окне | Разделить отказ до приложения и ошибку приложения; затем исправлять найденный слой |
| 504 после фиксированного интервала | Бюджет времени закончился на gateway, клиенте или зависимости | Сравнить таймауты границ и длительности span-ов | Найти участок, который исчерпал бюджет; не добавлять повтор вслепую |
| 200 и пустой экран | пустые данные, ошибка рендера или пустой HTML | Сопоставить response body, console error и DOM | Исправить контракт данных или рендер; отдельно проверить fallback |
| 403 для ожидаемого пользователя | решение политики не совпало с контекстом доступа | Проверить токен, claims, scope, ресурс и версию политики | Исправить конкретное условие; не ослаблять всю политику |
| Запись есть только в одном слое | потеря корреляции, sampling или отказ до следующей границы | Проверить формат полей, перенос заголовка и временное окно | Отметить разрыв как результат; добавить сигнал перед повтором |
Этот маршрут не восстанавливает данные, которых система не записала. Если gateway не переносит идентификатор, связь нельзя честно реконструировать по одному времени. Если trace sampling отбросил span, отсутствие span не означает отсутствие работы. Если прокси переписал статус или тело, нужно искать его access log и правила маршрутизации. Если несколько запросов выполняются параллельно, порядок строк в журнале не равен порядку причин.
Учебный классификатор не подходит как готовое правило блокировки или маршрутизации. Его ветки намеренно грубые. В реальной системе нужно учесть редиректы, retries, кеш, CDN, разные схемы авторизации и версию контракта API. Не добавляйте повторные попытки только потому, что ответ медленный: retry может увеличить нагрузку и скрыть первичный отказ. Не меняйте таймаут, пока не измерили бюджет на каждой границе.
Статус 502 или 504 также не доказывает, что downstream был недоступен. Причина может быть в несовместимом формате ответа, неверном DNS, закрытом соединении или ограничении шлюза. Статус 403 не доказывает, что пользователь «не имеет доступа» в бизнес-смысле: решение могло использовать устаревшие claims или другую версию политики. Проверяйте именно тот контекст, который использовал компонент.
Диагностика готова, когда другой инженер получает исходный конверт и может без устных пояснений назвать запрос, границу и следующий сигнал. Для исправления нужен ещё один результат: тот же сценарий проходит по ожидаемому пути, а отрицательный вариант по-прежнему получает правильный отказ. В журнале остаются идентификатор, статус, длительность и причина завершения. Если повтор только «стал зелёным», но различающий сигнал не сохранился, причина не доказана.