HTTP headers are lines of metadata that travel with every request and response. They are not part of the body, but they shape how the server and client understand each other: what data format is used, who is making the request, whether the response can be cached.
Standard headers
HTTP standardized the most common cases long ago. You don't need to reinvent them — you just need to use them correctly.
| Header | Where it's set | What it means |
|---|---|---|
Content-Type | request and response | the body format (application/json) |
Accept | request | the response format the client expects |
Authorization | request | the authentication token |
Location | 201 Created response | the URL of the created resource |
ETag | response | the resource version (a hash or a number) |
If-None-Match | request | "return the response only if the ETag has changed" |
If-Match | request | "modify the resource only if the ETag matches" |
Cache-Control | response | caching instructions |
A typical exchange looks like this:
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
When the server creates a resource, it returns 201 Created and tells the client where to find it:
HTTP/1.1 201 Created
Location: /api/v1/orders/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
The client shouldn't have to guess the URL — it takes it from Location and can immediately make a GET without an extra step.
Custom headers — with a domain prefix
Sometimes the standard headers aren't enough. For example, you may need to pass a request identifier from the client, a mobile app version, or a tenant identifier in a multi-tenant system.
For this you add custom headers. They used to be written with the X- prefix: X-Request-Id, X-Client-Version. In 2012, RFC 6648 officially declared this approach deprecated — X- says nothing about the header's ownership and creates confusion.
Instead of X-, use a domain prefix of the company or product — the same one across all services:
Shop-Request-Id: 550e8400-e29b-41d4-a716-446655440000
Shop-Client-Version: 2.1.0
Shop-Tenant-Id: acme
The prefix is chosen once and fixed in the team's standards. All services — order-service, payment-service, billing-service — use the same prefix. This makes it possible to tell a system header from a standard HTTP one at a glance.
A common mistake is confusing Shop-Request-Id with tracing. Shop-Request-Id identifies a specific request from a specific client (for deduplication and logging). Tracing is something else, covered below.
Idempotency-Key: safe request retries
Imagine this: the client sends a POST to create an order, but the network drops and the response never arrives. The client doesn't know whether the order was created or not. If it retries the request, a duplicate may appear.
Idempotency-Key solves this problem. The client generates a unique key once for a specific business operation and puts it in the header:
POST /api/v1/orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{ "items": [...] }
The server stores the key and the result of the operation. On a repeated request with the same key, the server returns the first result without creating a duplicate. If the client sends a different body with the same key, the server responds with 409 Conflict — because the key is already "taken" by another operation.
In Spring MVC the header is read like an ordinary parameter:
@PostMapping("/orders")
public ResponseEntity<OrderResponse> create(
@RequestHeader("Idempotency-Key") String idempotencyKey,
@RequestBody @Valid CreateOrderRequest request
) {
// ...
}
Idempotency-Key applies only to POST and PATCH — requests that change something. GET and DELETE are idempotent by definition (a repeated DELETE doesn't create a duplicate, it just returns 404).
traceparent — end-to-end tracing across services
When a user makes a request, it passes through several services: API gateway → order-service → payment-service → notification-service. If an error or delay occurs somewhere, you need to figure out at which step exactly.
Each team used to invent its own header — X-Trace-Id, X-Request-Id, Tracking-Id. The systems didn't understand each other. W3C standardized the format: traceparent.
traceparent: 00-1f2a8b6c7d3e4f5a9b0c1d2e3f4a5b6c-7a8b9c0d1e2f3a4b-01
│ │ │ │
│ trace-id (32 hex chars) span-id (16) flags
format version (always 00)
Here's what's what:
- trace-id — the unique identifier of the entire chain of calls from start to finish. The same trace-id passes through all services.
- span-id (called parent-id in the specification) — the identifier of the current step (span). Each service creates its own span-id.
- flags —
01means "the request is selected for recording" (sampled).
How a service handles an incoming request:
- The client sent a
traceparent→ take its trace-id, create a new span-id for your step, and pass it along. - The client didn't send one → generate a trace-id from scratch, create a span-id, and pass it along.
In Spring Boot this works automatically through OpenTelemetry:
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
opentelemetry-spring-boot-starter reads the incoming traceparent on its own, propagates it into outgoing HTTP requests and Kafka messages, and traceId and spanId show up in the MDC for logs.
One important point: the trace-id from traceparent is used as the traceId field in the error body (RFC 9457 format). This lets the client receive an error and immediately find the full request path in the tracing system by a single identifier.
Common mistakes
X- in custom headers. X-Request-Id, X-Client-Version are a deprecated style. Use a domain prefix: Shop-Request-Id, Shop-Client-Version.
Authorization without Bearer. JWT tokens are passed as Authorization: Bearer eyJhbGci.... The word Bearer is mandatory — it's part of the OAuth 2.0 standard.
Idempotency-Key on GET. Not needed: GET produces no side effects, a repeated request is always safe.
A homegrown tracing header instead of traceparent. Your own X-Trace-Id isn't understood by monitoring systems and other services. traceparent is a cross-industry standard.
A trace-id shorter than 32 characters. The W3C specification requires exactly 32 hex characters. 16 characters is a different, incompatible format.
In short
- HTTP headers carry metadata about the request and response — format, authentication, caching instructions.
Content-Type,Accept,Authorization,Location,ETag,If-None-Match,Cache-Controlare standard headers, each for its own purpose.Locationin201 Createdtells the client the URL of the created resource — without it the client won't know the address.- Custom headers are named with a domain prefix (
Shop-Request-Id), not withX-— RFC 6648 declared it deprecated back in 2012. Idempotency-Keymakes a POST request safe to retry: the same key → the same result, no duplicates.traceparent(W3C Trace Context) carries the trace-id through all the services in the chain; Spring Boot with OpenTelemetry handles it automatically.- The trace-id from
traceparentis used as thetraceIdin error bodies — it's what you use to find the full request path in the tracer.
Further reading
- Errors and status codes in REST API — how
traceIdgets into an RFC 9457 error body. - Idempotency in distributed systems — how to store and check the
Idempotency-Keyon the backend. - JSON and the response format — the full response structure, including
Locationfor 201.