How headers work
A header is a name-value pair sent before the body, in the form Name: value. Field names are
case-insensitive — Content-Type, content-type and
CONTENT-TYPE are the same field — although HTTP/2 and HTTP/3 require lowercase on the wire and
compress them with HPACK or QPACK rather than sending text. Values are generally case-sensitive, and some
fields may legally appear more than once, in which case the values combine as a comma-separated list. The
big exception is Set-Cookie, which must be sent as repeated separate lines.
Headers split roughly into four groups: request headers that describe the client or shape
the response, response headers that describe the server or the resource,
representation headers such as Content-Type and Content-Encoding
that describe the body and can appear on either side, and hop-by-hop headers such as
Connection and Transfer-Encoding that apply to a single connection and are
consumed by each proxy rather than forwarded.
One convention worth unlearning: do not prefix custom headers with X-. RFC 6648 deprecated that
in 2012 precisely because standardisation forces a rename and both spellings then live forever — which is
exactly what happened to X-Forwarded-For and its successor Forwarded.
Request headers
These travel from client to server. In the REST API tester you set them in
the Headers tab; the Auth tab is a shortcut that builds an Authorization header for you.
| Header | Purpose | Example |
|---|---|---|
Host | Target hostname and port. Mandatory in HTTP/1.1; how one IP serves many sites | api.example.com |
Content-Type | Media type of the body being sent | application/json; charset=utf-8 |
Content-Length | Body size in bytes | 348 |
Accept | Media types the client will accept back | application/json, text/plain;q=0.8 |
Accept-Encoding | Compression the client can decode | gzip, br |
Accept-Language | Preferred natural languages | en-GB, en;q=0.9 |
Authorization | Credentials for the target resource | Bearer eyJhbGciOi… |
User-Agent | Identifies the client software | MyApp/2.1 (+https://example.com) |
Origin | Scheme, host and port initiating a cross-origin request | https://app.example.com |
Referer | URL of the page that produced the request (spelled wrong since 1996) | https://example.com/docs |
Cookie | Cookies previously set for this origin | session=abc123 |
If-None-Match | Conditional GET — send the body only if the ETag differs | "a1b2c3" |
If-Match | Conditional write — proceed only if the ETag still matches | "a1b2c3" |
If-Modified-Since | Date-based conditional GET | Wed, 21 Oct 2026 07:28:00 GMT |
Range | Request part of a resource; answered with 206 | bytes=0-1023 |
Idempotency-Key | De facto convention letting a client retry a POST safely | 7f3c1e2a-… |
Content-Type: the one that breaks most requests
Content-Type tells the receiver how to parse the bytes. Send a JSON body without it, or with
the wrong value, and a strict server answers 415 Unsupported Media Type
— or worse, a lenient framework parses it as form data and you get a
400 about missing fields that are plainly present. The four you meet
constantly:
| Value | Used for | Body shape |
|---|---|---|
application/json | Almost every modern API | {"name":"Ada"} |
application/x-www-form-urlencoded | HTML forms, OAuth token endpoints | name=Ada&role=admin |
multipart/form-data | File uploads; needs a boundary parameter | Boundary-delimited parts |
text/plain | Logs, plain payloads | Raw text |
Note that OAuth 2.0 token endpoints take application/x-www-form-urlencoded, not JSON — a
surprisingly common cause of a token request failing against a perfectly correct client. If your JSON body is
being rejected, paste it into the JSON formatter first to rule out a
syntax error such as a trailing comma.
Accept and content negotiation
Accept is the request-side counterpart: it lists what the client wants back, with optional
quality values from 0 to 1 expressing preference.
Accept: application/json;q=1.0, text/xml;q=0.5, */*;q=0.1 means "JSON ideally, XML if you must,
anything rather than nothing". A server that cannot satisfy the list may return
406 Not Acceptable, though most just send JSON regardless. Many APIs
also use Accept for versioning, as in
Accept: application/vnd.example.v2+json.
Authorization
The value is a scheme name followed by credentials. The three you will meet:
| Scheme | Format | Notes |
|---|---|---|
Bearer | Bearer <token> | OAuth 2.0 and JWTs. Whoever holds the token is the caller — no signature to prove otherwise |
Basic | Basic base64(user:pass) | Base64 is encoding, not encryption. Only safe over TLS |
Digest | Challenge-response with a nonce | Largely historical; avoids sending the password, but rarely used today |
Plenty of APIs skip Authorization entirely and use a custom header such as
X-API-Key — the tester's Auth tab supports that too. When a bearer token is rejected, decode it
with the JWT decoder and check the exp claim before assuming
the server is wrong; expiry explains most sudden 401s. For Basic, the
Base64 encoder will build or unpick the credential string.
Response headers
| Header | Purpose | Example |
|---|---|---|
Content-Type | Media type of the returned body | application/json; charset=utf-8 |
Content-Encoding | Compression applied to the body | gzip |
Cache-Control | Caching rules for clients and proxies | public, max-age=3600 |
ETag | Opaque version identifier for this representation | "a1b2c3" or W/"a1b2c3" |
Last-Modified | When the resource last changed | Wed, 21 Oct 2026 07:28:00 GMT |
Location | URL of a new resource, or a redirect target | /v1/orders/42 |
Allow | Methods this resource supports; required with 405 | GET, PUT, DELETE |
WWW-Authenticate | Challenge accompanying a 401 | Bearer realm="api", error="invalid_token" |
Retry-After | When to try again; seconds or an HTTP date | 120 |
Vary | Request headers that change the response; critical for caches | Accept-Encoding, Origin |
Set-Cookie | Stores a cookie; repeat the header per cookie | session=abc; HttpOnly; Secure; SameSite=Lax |
Strict-Transport-Security | Forces HTTPS for this host in future | max-age=63072000; includeSubDomains |
Content-Disposition | Inline display or download with a filename | attachment; filename="report.csv" |
Cache-Control
The most misread header in HTTP, mostly because of one directive. no-cache does
not mean "do not cache" — it means a cache may store the response but must revalidate with the
origin before reusing it. The directive that forbids storage is no-store.
| Directive | Meaning |
|---|---|
max-age=N | Fresh for N seconds from the time of the response |
s-maxage=N | Same, but only for shared caches; overrides max-age there |
no-cache | May be stored, must be revalidated before reuse |
no-store | Must not be written to disk or memory anywhere. Use for sensitive data |
private | Browser may cache it; shared caches and CDNs may not |
public | Any cache may store it, even if the request was authenticated |
must-revalidate | Once stale, never serve without checking the origin |
stale-while-revalidate=N | Serve stale for up to N seconds while refreshing in the background |
immutable | Content will never change; skip revalidation entirely |
For an API returning per-user data, Cache-Control: private, no-store is the safe default. For
genuinely public reference data, public, max-age=300 plus an ETag gives you cheap
revalidation: the client sends the ETag back as If-None-Match and the server answers
304 Not Modified with no body at all.
Whenever a response varies by a request header, say so with Vary. A cache that stores an English
response and serves it to a client asking for French — or stores one tenant's data and serves it to another —
is nearly always a missing Vary.
CORS headers
Cross-Origin Resource Sharing is how a server tells the browser that a page from another origin is allowed to
read its responses. It is enforced entirely by the browser: the request usually still reaches the server and
still has effects, the browser just refuses to hand the response to the JavaScript that asked for it. That is
why a request failing with a CORS error in the console works perfectly from curl — and why the
API tester proxies requests through its own server, which is not a browser and is not bound
by CORS.
| Header | Side | Purpose |
|---|---|---|
Origin | Request | Origin the request came from; set by the browser, not the page |
Access-Control-Request-Method | Preflight | Method the real request intends to use |
Access-Control-Request-Headers | Preflight | Custom headers the real request intends to send |
Access-Control-Allow-Origin | Response | Origin permitted to read the response, or * |
Access-Control-Allow-Methods | Preflight response | Methods permitted cross-origin |
Access-Control-Allow-Headers | Preflight response | Request headers permitted cross-origin |
Access-Control-Allow-Credentials | Response | true to permit cookies and Authorization |
Access-Control-Expose-Headers | Response | Response headers JavaScript is allowed to read |
Access-Control-Max-Age | Preflight response | Seconds the browser may cache the preflight result |
The preflight
A request is "simple" if it uses GET, HEAD or POST with only a handful
of allowed headers and a basic content type. Anything else — a PUT, an
Authorization header, Content-Type: application/json — triggers a preflight: the
browser first sends an OPTIONS request announcing what it
intends to do, and only proceeds if the response permits it. Three failure modes cover most CORS bugs:
- The preflight is not handled. Your framework routes
POST /v1/ordersbut returns 404 or 405 forOPTIONS /v1/orders, so the real request never happens. Access-Control-Allow-Headersomits a header you send. The list must name every non-simple header,authorizationandcontent-typeincluded. It is not implied byAllow-Origin.- Wildcard with credentials. If the request carries cookies or an
Authorizationheader,Access-Control-Allow-Origin: *is rejected. The server must echo the exact origin, addAccess-Control-Allow-Credentials: true, and sendVary: Originso a cache never serves one origin's response to another.
Also remember that JavaScript can only read a short default list of response headers. If your client needs
X-Request-Id or a rate-limit header, name it in Access-Control-Expose-Headers or it
will appear to be missing even though it is on the wire.
Rate-limit headers
Rate limiting is nearly universal and, until recently, entirely unstandardised. The de facto convention uses
the X-RateLimit- prefix; the IETF's newer RateLimit fields drop it. You will meet
both, and plenty of vendor-specific variants besides.
| Header | Meaning | Example |
|---|---|---|
X-RateLimit-Limit | Requests allowed in the current window | 5000 |
X-RateLimit-Remaining | Requests left in this window | 4987 |
X-RateLimit-Reset | When the window resets — often a Unix timestamp, sometimes seconds | 1793491200 |
RateLimit-Limit | Standardising equivalent of the above | 100 |
RateLimit-Remaining | Standardising equivalent | 87 |
RateLimit-Reset | Seconds until the window resets | 42 |
Retry-After | Standard since HTTP/1.1; seconds or an HTTP date | 60 |
The trap is Reset: GitHub sends a Unix epoch timestamp, others send seconds remaining, and a
client that guesses wrong either hammers the API or sleeps for fifty years. Check the documentation, then
confirm against a real response. Retry-After is the one to trust when present — it accompanies
429 Too Many Requests and
503 Service Unavailable and its units are unambiguous.
A well-behaved client reads these proactively rather than waiting to be rejected: when
Remaining gets low, slow down. When you do get a 429, back off exponentially with
jitter — synchronised retries from many clients are how a brief limit becomes an outage.
Inspecting headers in practice
Send a request from the online API tester and the response pane lists every header the server
returned, which is usually faster than reopening devtools. Two habits pay off immediately. First, use
HEAD when you only care about metadata — same headers, no body to scroll past. Second, when a
request behaves differently in your app than in the tester, diff the headers rather than the code; a missing
Accept, an extra Cookie or a framework-injected Content-Type is
usually the whole difference.
For the semantics behind the methods these headers accompany, see the HTTP methods reference. To turn ad-hoc poking into a repeatable suite, read the REST API testing guide. And when a response comes back with a code you cannot place, the HTTP status code reference lists every one.
Frequently asked questions
What is the difference between Accept and Content-Type?
Content-Type describes the body of the message carrying it. Accept appears only
on requests and states what the client wants back. A request commonly has both.
Are header names case-sensitive?
Names are case-insensitive; HTTP/2 and HTTP/3 require lowercase on the wire. Values are generally case-sensitive.
Does Cache-Control: no-cache mean do not cache?
No — it permits storage but requires revalidation before reuse. no-store is the directive
that forbids storing the response.
Why does Access-Control-Allow-Origin: * fail with credentials?
The CORS spec forbids the wildcard on credentialed requests. Echo the exact origin, send
Access-Control-Allow-Credentials: true, and add Vary: Origin.
Should I prefix custom headers with X-?
No. RFC 6648 deprecated it in 2012. Use a descriptive unprefixed name namespaced to your product.
Why can't my JavaScript read a header that is clearly in the response?
Cross-origin, the browser only exposes a short default list. The server must name any other header in
Access-Control-Expose-Headers.