Headers are instructions, not decoration
A response body is bytes. Every decision about what those bytes are — text or an image, which character set, whether to store them, whether to show them or download them, whether another site may read them — is made from the headers. Get one wrong and the body is perfect and the page is broken.
Describing the body
Content-Disposition: attachment turns a response into a download instead of a page — and is also the safe way to serve a file a user uploaded, because the browser will not execute it as HTML.
Content-Type: text/html; charset=utf-8
Content-Type: application/json
Content-Length: 2048
Content-Encoding: br # the body is compressed (brotli)
Content-Disposition: attachment; filename="prices.csv"
text/html without charset=utf-8 makes the browser guess, and a guess is how a page full of Azerbaijani text turns into question marks. It is one string in one place; set it everywhere, including on JSON.
Asking for something specific
A server that varies its answer on any of these must say so with Vary, or a cache will hand the Azerbaijani page to an English reader. Vary: Accept-Encoding, Accept-Language is the usual minimum.
Accept: application/json # what I can handle
Accept-Language: az, en;q=0.8 # in this order of preference
Accept-Encoding: gzip, br # I can decompress these
Range: bytes=0-1023 # just this part
If-None-Match: "v3" # only if it changed
Authorization: Bearer eyJhbGciOi…
User-Agent: … # a long and unreliable story
The ones that change behaviour
| Header | Effect |
|---|---|
Cache-Control | whether and how long this may be stored — the next lesson |
Set-Cookie | state that comes back on later requests |
Location | where a 3xx points |
Access-Control-Allow-Origin | whether another origin may read this |
Content-Security-Policy | what the page is allowed to load and run |
Strict-Transport-Security | https only, from now on |
X-Content-Type-Options: nosniff | do not second-guess the content type |
Retry-After | when to try again after a 429 or a 503 |
Case, repeats and the X- prefix
Header names are case-insensitive — Content-Type and content-type are the same header, and HTTP/2 requires them lower-case on the wire. Some may appear more than once (Set-Cookie always does); the Fetch API joins repeats with a comma, except Set-Cookie, which JavaScript cannot read at all.
The X- prefix for custom headers was deprecated in 2012 — X-Request-Id survives by habit. A custom header on a cross-origin request triggers a CORS preflight, which is a real cost: see the CORS lesson.
const res = await fetch("/api/orders");
res.headers.get("content-type"); // case does not matter
res.headers.has("x-request-id");
[...res.headers]; // only the CORS-safelisted ones, cross-origin