HTTP headers

Headers are the metadata attached to every HTTP request and response — content type, authentication, caching rules, CORS permissions, rate limits. Most confusing API behaviour is explained by a header somebody sent or forgot. This guide covers the ones that matter, with examples you can reproduce in the API tester.

How headers work

A header is a name-value pair sent before the body, in the form Name: value. Field names are case-insensitiveContent-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.

HeaderPurposeExample
HostTarget hostname and port. Mandatory in HTTP/1.1; how one IP serves many sitesapi.example.com
Content-TypeMedia type of the body being sentapplication/json; charset=utf-8
Content-LengthBody size in bytes348
AcceptMedia types the client will accept backapplication/json, text/plain;q=0.8
Accept-EncodingCompression the client can decodegzip, br
Accept-LanguagePreferred natural languagesen-GB, en;q=0.9
AuthorizationCredentials for the target resourceBearer eyJhbGciOi…
User-AgentIdentifies the client softwareMyApp/2.1 (+https://example.com)
OriginScheme, host and port initiating a cross-origin requesthttps://app.example.com
RefererURL of the page that produced the request (spelled wrong since 1996)https://example.com/docs
CookieCookies previously set for this originsession=abc123
If-None-MatchConditional GET — send the body only if the ETag differs"a1b2c3"
If-MatchConditional write — proceed only if the ETag still matches"a1b2c3"
If-Modified-SinceDate-based conditional GETWed, 21 Oct 2026 07:28:00 GMT
RangeRequest part of a resource; answered with 206bytes=0-1023
Idempotency-KeyDe facto convention letting a client retry a POST safely7f3c1e2a-…

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:

ValueUsed forBody shape
application/jsonAlmost every modern API{"name":"Ada"}
application/x-www-form-urlencodedHTML forms, OAuth token endpointsname=Ada&role=admin
multipart/form-dataFile uploads; needs a boundary parameterBoundary-delimited parts
text/plainLogs, plain payloadsRaw 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:

SchemeFormatNotes
BearerBearer <token>OAuth 2.0 and JWTs. Whoever holds the token is the caller — no signature to prove otherwise
BasicBasic base64(user:pass)Base64 is encoding, not encryption. Only safe over TLS
DigestChallenge-response with a nonceLargely 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

HeaderPurposeExample
Content-TypeMedia type of the returned bodyapplication/json; charset=utf-8
Content-EncodingCompression applied to the bodygzip
Cache-ControlCaching rules for clients and proxiespublic, max-age=3600
ETagOpaque version identifier for this representation"a1b2c3" or W/"a1b2c3"
Last-ModifiedWhen the resource last changedWed, 21 Oct 2026 07:28:00 GMT
LocationURL of a new resource, or a redirect target/v1/orders/42
AllowMethods this resource supports; required with 405GET, PUT, DELETE
WWW-AuthenticateChallenge accompanying a 401Bearer realm="api", error="invalid_token"
Retry-AfterWhen to try again; seconds or an HTTP date120
VaryRequest headers that change the response; critical for cachesAccept-Encoding, Origin
Set-CookieStores a cookie; repeat the header per cookiesession=abc; HttpOnly; Secure; SameSite=Lax
Strict-Transport-SecurityForces HTTPS for this host in futuremax-age=63072000; includeSubDomains
Content-DispositionInline display or download with a filenameattachment; 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.

DirectiveMeaning
max-age=NFresh for N seconds from the time of the response
s-maxage=NSame, but only for shared caches; overrides max-age there
no-cacheMay be stored, must be revalidated before reuse
no-storeMust not be written to disk or memory anywhere. Use for sensitive data
privateBrowser may cache it; shared caches and CDNs may not
publicAny cache may store it, even if the request was authenticated
must-revalidateOnce stale, never serve without checking the origin
stale-while-revalidate=NServe stale for up to N seconds while refreshing in the background
immutableContent 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.

HeaderSidePurpose
OriginRequestOrigin the request came from; set by the browser, not the page
Access-Control-Request-MethodPreflightMethod the real request intends to use
Access-Control-Request-HeadersPreflightCustom headers the real request intends to send
Access-Control-Allow-OriginResponseOrigin permitted to read the response, or *
Access-Control-Allow-MethodsPreflight responseMethods permitted cross-origin
Access-Control-Allow-HeadersPreflight responseRequest headers permitted cross-origin
Access-Control-Allow-CredentialsResponsetrue to permit cookies and Authorization
Access-Control-Expose-HeadersResponseResponse headers JavaScript is allowed to read
Access-Control-Max-AgePreflight responseSeconds 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/orders but returns 404 or 405 for OPTIONS /v1/orders, so the real request never happens.
  • Access-Control-Allow-Headers omits a header you send. The list must name every non-simple header, authorization and content-type included. It is not implied by Allow-Origin.
  • Wildcard with credentials. If the request carries cookies or an Authorization header, Access-Control-Allow-Origin: * is rejected. The server must echo the exact origin, add Access-Control-Allow-Credentials: true, and send Vary: Origin so 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.

HeaderMeaningExample
X-RateLimit-LimitRequests allowed in the current window5000
X-RateLimit-RemainingRequests left in this window4987
X-RateLimit-ResetWhen the window resets — often a Unix timestamp, sometimes seconds1793491200
RateLimit-LimitStandardising equivalent of the above100
RateLimit-RemainingStandardising equivalent87
RateLimit-ResetSeconds until the window resets42
Retry-AfterStandard since HTTP/1.1; seconds or an HTTP date60

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.