HTTP-заголовки — это строки метаданных, которые идут вместе с каждым запросом и ответом. Они не входят в тело, но влияют на то, как сервер и клиент понимают друг друга: какой формат данных, кто делает запрос, можно ли кешировать ответ.
Стандартные заголовки
HTTP давно стандартизировал самые нужные случаи. Их не надо изобретать — достаточно использовать правильно.
| Заголовок | Где ставится | Что означает |
|---|---|---|
Content-Type | запрос и ответ | формат тела (application/json) |
Accept | запрос | какой формат ответа ждёт клиент |
Authorization | запрос | токен аутентификации |
Location | ответ 201 Created | URL созданного ресурса |
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.