DarkRiDDeR15 мин

Где рождается ошибка HTTP: разбираем цепочку DNS, TLS и заголовков

СетиHTTP

Когда браузер показывает «не удалось подключиться», виден только итог. Цена ошибки — изменить код приложения, не проверив, что запрос вообще не прошёл TLS, или принять ответ кэша за ответ origin-сервера. Для точной диагностики нужно восстановить цепочку по наблюдаемым границам, а не угадывать виновника по одному коду.

В этой статье разложим запрос на переходы и посмотрим, какие поля подтверждают каждый переход. Практический результат — короткая таблица: какой лог или команда отвечает на конкретный вопрос. Пример запускается локально на Node и показывает обмен HTTP-заголовками без обращения к внешнему узлу.

Пять границ одного запроса

DNS превращает имя в адрес, TCP устанавливает поток байтов, TLS защищает его и связывает с именем, HTTP передаёт метод и путь, а приложение формирует ответ. Посредник может добавить свой статус или заголовок на каждом шаге. Поэтому поле server в ответе не доказывает, что ответ сформирован именно вашим приложением.

Поле и вопрос диагностики
ГраницаЧто можно утверждатьЧто проверить
DNSимя разрешилось в адресответ A/AAAA и выбранный адрес
TCPпорт принял соединениеconnect error и время установки
TLSсертификат подходит имениSAN, chain, protocol version
HTTPполучен статус и заголовкиmethod, path, status, headers
Приложениеобработан контракт endpointлог маршрута и request id
Матрица симптомов HTTP и TLS с границей, которую подтверждает каждый вид наблюдения.
У каждой строки есть отдельный вопрос. Нельзя переносить ответ из соседней строки: HTTP-статус не подтверждает TLS, а DNS-ответ не подтверждает маршрут.

Заголовок не равен доказательству источника

Заголовок Via, Server или пользовательский X-Request-Id помогает построить гипотезу, но это данные сообщения, а не криптографическая аттестация сервера. Посредник может удалить или переписать поля. Для сопоставления запроса с серверным логом нужен идентификатор, который генерируется на входе доверенного компонента и сохраняется без смены формата.

У HTTP/2 и HTTP/3 целевой authority может передаваться не так, как привычная строка Host. Поэтому в диагностической записи сохраняйте логическое имя назначения и не делайте вывод о виртуальном хосте по одному полю. Сопоставляйте его с тем, что использовал TLS-клиент.

Удобная минимальная запись выглядит так: метод, нормализованный путь, статус, request id, длительность, размер ответа и класс ошибки. Секреты и произвольные query-параметры не входят в журнал по умолчанию. Нормализация важна: если один компонент пишет полный URL, а другой — только путь, поиск по записи будет давать ложные пропуски.

Учебный обмен с заголовками

Локальный сервер ниже возвращает два заголовка и JSON-тело. Вход — HTTP-запрос с Accept. Ожидаемый результат — 200, тип содержимого и один идентификатор. Это реальный обмен между клиентом и сервером, но он не проверяет TLS или поведение прокси.

import { createServer } from 'node:http';

const server = createServer((request, response) => {
  response.writeHead(200, {
    'content-type': 'application/json; charset=utf-8',
    'x-request-id': 'local-001',
  });
  response.end(JSON.stringify({ method: request.method, path: request.url }));
});

server.listen(0, 'localhost', async () => {
  const port = server.address().port;
  const endpoint = 'http://localhost:' + port + '/orders';
  const response = await fetch(endpoint);
  console.log(response.status, response.headers.get('x-request-id'));
  console.log(await response.json());
  server.close();
});

Результат содержит 200 local-001 и объект с методом GET и путём /orders. Если убрать заголовок из ответа, клиент всё равно получит 200: это показывает, что request id — средство сопоставления, а не условие корректности HTTP. В рабочей системе нужно договориться, какой компонент отвечает за его создание и где он попадает в лог.

TLS меняет порядок проверки

При HTTPS нельзя начинать с ответа приложения. Клиент сначала отправляет ClientHello, сервер выбирает параметры и предъявляет сертификат, затем стороны завершают рукопожатие. Проверка имени происходит относительно hostname, который клиент считает целевым. Если соединение идёт через proxy, отдельно фиксируйте имя proxy и имя origin: это две разные проверки.

Сертификат с правильной цепочкой, но неправильным SAN — ошибка имени. Сертификат с правильным SAN, который не доверен локальному хранилищу, — ошибка доверия. Сертификат с истёкшим сроком — ошибка времени. Эти причины нельзя объединять в «проблему SSL»: для каждой нужен собственный ожидаемый результат и свой владелец исправления.

Кэш и промежуточный ответ

HTTP допускает intermediaries (посредников), а RFC 9110 отдельно описывает кэширование и маршрутизацию. Ответ может быть свежим объектом кэша, перенаправлением или сообщением origin. Проверяйте Age, Cache-Control, ETag, Via и время изменения тела, но не делайте вывод о кэше по одному заголовку: политика конкретного посредника может быть сложнее.

Набор безопасных полей для записи
ПолеПримерРиск при сборе
methodGETмалый, если путь обезличен
path/ordersquery может содержать секрет
status200не показывает причину сам по себе
durationMs42нужно знать часы и границы замера
requestIdlocal-001нельзя принимать внешний id без правила
responseSize31не заменяет проверку тела

Порядок проверки по границам

  1. Зафиксировать имя, адрес и режим proxy отдельно; query-параметры очистить.
  2. Проверить DNS и TCP независимым клиентом, сохранив только код ошибки и длительность.
  3. Для HTTPS сверить hostname, SAN, цепочку и время действия сертификата.
  4. Снять HTTP-статус и выбранные заголовки, затем сопоставить request id с логом доверенного входа.
  5. Сравнить ответ с origin и кэшем, если между клиентом и приложением есть intermediary.
  6. Записать причину только на том уровне, который подтверждён наблюдением.

Ограничения и следующий шаг

Локальный обмен не показывает потери пакетов, балансировку, корпоративный proxy и особенности браузерного хранилища. Поле x-request-id в учебном сервере задано вручную; в настоящем сервисе его происхождение и доверенная зона должны быть описаны отдельно. Не смешивайте диагностическую метку с секретом или пользовательским идентификатором.

Следующий шаг — добавить к одной безопасной проверке два вывода: сетевой этап и серверный лог. Сначала докажите, что запрос достиг доверенного входа, потом связывайте его с handler. Такой порядок уменьшает область поиска и не заставляет приложение отвечать за сбой, произошедший раньше.

Проверяемые источники

  • RFC 9110 — HTTP Semantics — IETF Standards Track, June 2022. Нужен для различения методов, статусов, заголовков, маршрутизации и ответа посредника. Граница применимости: Не объясняет конкретную конфигурацию прокси, DNS или причину ошибки в вашем сервисе.
  • RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3 — IETF Standards Track, August 2018. Нужен для разбора рукопожатия TLS 1.3, проверки имени узла и сообщения об ошибке сертификата. Граница применимости: Не подтверждает доверие к конкретному центру сертификации и не заменяет проверку ключевого материала.