← назад к разделу

HTTP-заголовки — это строки метаданных, которые идут вместе с каждым запросом и ответом. Они не входят в тело, но влияют на то, как сервер и клиент понимают друг друга: какой формат данных, кто делает запрос, можно ли кешировать ответ.

Стандартные заголовки

HTTP давно стандартизировал самые нужные случаи. Их не надо изобретать — достаточно использовать правильно.

ЗаголовокГде ставитсяЧто означает
Content-Typeзапрос и ответформат тела (application/json)
Acceptзапроскакой формат ответа ждёт клиент
Authorizationзапростокен аутентификации
Locationответ 201 CreatedURL созданного ресурса
ETagответверсия ресурса (хеш или номер)
If-None-Matchзапрос«верни ответ только если ETag изменился»
If-Matchзапрос«измени ресурс только если ETag совпадает»
Cache-Controlответинструкции для кеширования

Типичный обмен выглядит так:

GET /api/v1/orders/550e8400
Accept: application/json
Authorization: Bearer eyJhbGci...
If-None-Match: "33a64df5"

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "33a64df5"
Cache-Control: private, max-age=60

Когда сервер создаёт ресурс, он возвращает 201 Created и сообщает клиенту, где его найти:

HTTP/1.1 201 Created
Location: /api/v1/orders/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

Клиент не должен угадывать URL — он берёт его из Location и сразу может сделать GET без лишнего шага.

Собственные заголовки — с доменным префиксом

Иногда стандартных заголовков не хватает. Например, нужно передать идентификатор запроса от клиента, версию мобильного приложения или идентификатор арендатора в мультиарендной системе.

Для этого добавляют собственные заголовки. Раньше их писали с префиксом X-: X-Request-Id, X-Client-Version. В 2012 году RFC 6648 официально признал этот подход устаревшим — X- ничего не говорит о принадлежности заголовка и создаёт путаницу.

Вместо X- используют доменный префикс компании или продукта — одинаковый для всех сервисов:

Shop-Request-Id: 550e8400-e29b-41d4-a716-446655440000
Shop-Client-Version: 2.1.0
Shop-Tenant-Id: acme

Префикс выбирается один раз и фиксируется в стандартах команды. Все сервисы — order-service, payment-service, billing-service — используют один и тот же префикс. Это позволяет сразу отличить системный заголовок от стандартного HTTP.

Частая ошибка — не путать Shop-Request-Id и трассировку. Shop-Request-Id идентифицирует конкретный запрос от конкретного клиента (для дедупликации и логирования). Трассировка — это другое, о ней ниже.

Idempotency-Key: безопасный повторный запрос

Представьте: клиент отправляет POST для создания заказа, но сеть обрывается и ответ не приходит. Клиент не знает — заказ создался или нет. Если он повторит запрос, может появиться дубль.

Idempotency-Key решает эту проблему. Клиент генерирует уникальный ключ один раз для конкретной бизнес-операции и кладёт его в заголовок:

POST /api/v1/orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{ "items": [...] }

Сервер сохраняет ключ и результат операции. При повторном запросе с тем же ключом сервер возвращает первый результат, не создавая дубль. Если же клиент пришлёт другое тело с тем же ключом, сервер ответит 409 Conflict — потому что ключ уже «занят» другой операцией.

В Spring MVC заголовок читается как обычный параметр:

@PostMapping("/orders")
public ResponseEntity<OrderResponse> create(
    @RequestHeader("Idempotency-Key") String idempotencyKey,
    @RequestBody @Valid CreateOrderRequest request
) {
    // ...
}

Idempotency-Key применяется только к POST и PATCH — запросам, которые что-то меняют. GET и DELETE идемпотентны по определению (повторный DELETE не создаёт дубль, просто возвращает 404).

traceparent — сквозная трассировка между сервисами

Когда пользователь делает запрос, он проходит через несколько сервисов: API-шлюз → order-service → payment-service → notification-service. Если где-то возникает ошибка или задержка, нужно понять, на каком именно шаге.

Раньше каждая команда изобретала свой заголовок — X-Trace-Id, X-Request-Id, Tracking-Id. Системы не понимали друг друга. W3C стандартизировал формат: traceparent.

traceparent: 00-1f2a8b6c7d3e4f5a9b0c1d2e3f4a5b6c-7a8b9c0d1e2f3a4b-01
             │  │                                │                │
             │  trace-id (32 hex-символа)         span-id (16)    флаги
             версия формата (всегда 00)

Что тут что:

  • trace-id — уникальный идентификатор всей цепочки вызовов от начала до конца. Один и тот же trace-id проходит через все сервисы.
  • span-id (в спецификации называется parent-id) — идентификатор текущего шага (span). Каждый сервис создаёт свой span-id.
  • флаги01 означает «запрос выбран для записи» (sampled).

Как сервис обрабатывает входящий запрос:

  • Клиент прислал traceparent → берём его trace-id, создаём новый span-id для своего шага и передаём дальше.
  • Клиент не прислал → генерируем trace-id с нуля, создаём span-id и передаём дальше.

В Spring Boot это работает автоматически через OpenTelemetry:

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>

opentelemetry-spring-boot-starter сам читает входящий traceparent, прокидывает его в исходящие HTTP-запросы и Kafka-сообщения, а traceId и spanId появляются в MDC для логов.

Один важный момент: trace-id из traceparent используется как поле traceId в теле ошибки (формат RFC 9457). Это позволяет клиенту получить ошибку и сразу найти полный путь запроса в системе трассировки по одному идентификатору.

Типичные ошибки

X- в собственных заголовках. X-Request-Id, X-Client-Version — устаревший стиль. Используйте доменный префикс: Shop-Request-Id, Shop-Client-Version.

Authorization без Bearer. JWT-токены передаются как Authorization: Bearer eyJhbGci.... Слово Bearer обязательно — это часть стандарта OAuth 2.0.

Idempotency-Key на GET. Не нужен: GET не создаёт побочных эффектов, повторный запрос всегда безопасен.

Самописный заголовок трассировки вместо traceparent. Свой X-Trace-Id не понимают системы мониторинга и другие сервисы. traceparent — межотраслевой стандарт.

trace-id короче 32 символов. Спецификация W3C требует ровно 32 hex-символа. 16 символов — другой формат, несовместимый.

Коротко

  • HTTP-заголовки несут метаданные запроса и ответа — формат, аутентификацию, инструкции кеширования.
  • Content-Type, Accept, Authorization, Location, ETag, If-None-Match, Cache-Control — стандартные заголовки, каждый по своему назначению.
  • Location в 201 Created сообщает клиенту URL созданного ресурса — без него клиент не узнает адрес.
  • Собственные заголовки именуют с доменным префиксом (Shop-Request-Id), не с X- — RFC 6648 признал его устаревшим ещё в 2012 году.
  • Idempotency-Key делает POST-запрос безопасным при повторе: одинаковый ключ → тот же результат, без дублей.
  • traceparent (W3C Trace Context) несёт trace-id через все сервисы цепочки; Spring Boot с OpenTelemetry обрабатывает его автоматически.
  • trace-id из traceparent используется как traceId в теле ошибок — по нему находят полный путь запроса в трассировщике.

Что почитать дальше

  • Ошибки и коды статусов в REST API — как traceId попадает в тело ошибки RFC 9457.
  • Идемпотентность в распределённых системах — как хранить и проверять Idempotency-Key на backend.
  • JSON и формат ответов — полная структура ответа, в том числе Location для 201.