Content negotiation and payload
This group describes the body: what format it is in, how it was compressed, how long it is, and what the client would have preferred. Most APIs use about a third of it and ignore the rest, which is a defensible choice — the trouble starts when you half-implement negotiation and forget the Vary that makes it safe behind a cache.
Two distinctions are worth burning in. Content-Encoding is end-to-end compression that the client decodes; Transfer-Encoding is per-hop framing that any proxy may undo. And Content-Location is not Location: one tells you where the representation you just got actually lives, the other tells the client to go somewhere else.
| Header | What it does | Example |
|---|---|---|
| Accept Request | Media types the client can handle, with q-weights expressing preference. If nothing matches, the honest answer is 406. Most APIs ignore this and always return JSON, which is fine — just do not also advertise negotiation you do not do. | Accept: application/json;q=1.0, text/plain;q=0.5, */*;q=0.1 |
| Accept-Encoding Request | Compression formats the client understands. The reason Vary: Accept-Encoding is mandatory on anything you compress at the origin. | Accept-Encoding: gzip, br, zstd |
| Accept-Language Request | Preferred human languages, weighted. Serving different copy per language without Vary: Accept-Language is the classic way to show a French page to an English visitor straight out of the cache. | Accept-Language: en-GB, en;q=0.9, fr;q=0.7 |
| Accept-Charset Request Dead | Character encodings the client accepts. Deprecated in RFC 9110. Browsers stopped sending it years ago, and everything is UTF-8. | Accept-Charset: utf-8 |
| Content-Type Request + response | The media type of the body, plus parameters such as charset and multipart boundary. Always send charset on text types. A JSON body sent with no Content-Type is the single most common cause of a mysterious 415. | Content-Type: application/json; charset=utf-8 |
| Content-Encoding Response | How the body was compressed. End-to-end: it stays compressed until the client decodes it. Not the same thing as Transfer-Encoding, which is per-hop and can be undone by any proxy on the path. | Content-Encoding: br |
| Content-Language Response | Which language the body is in — the answer to Accept-Language, and one of the seven headers CORS exposes by default. | Content-Language: en-GB |
| Content-Length Request + response | Body size in bytes. Sending Content-Length and Transfer-Encoding: chunked on the same HTTP/1.1 message is the foundation of request smuggling. A front end should reject that message, not pick a winner. | Content-Length: 34871 |
| Content-Disposition Response | Render inline or download, and what to call the file. For non-ASCII names use the filename* form with UTF-8 percent-encoding. Sanitise it either way — an unescaped filename is a header-injection hole. | Content-Disposition: attachment; filename="q3-report.csv" |
| Content-Location Response | The direct URL of the representation you just received, when it differs from the URL you asked for. Not Location, and it redirects nothing. Mixing the two up produces bugs that survive code review because the names look alike. | Content-Location: /reports/q3.json |
| Content-Digest Request + response | A checksum of the body, carried as a structured field. RFC 9530, replacing the old Digest header. Useful on webhooks and uploads; compare in constant time when it feeds a signature check. | Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: |
| Transfer-Encoding Request + response | How the message body was framed on this hop — in practice, chunked. HTTP/1.1 only. Forbidden in HTTP/2 and HTTP/3, which do their own framing. | Transfer-Encoding: chunked |
| TE Request | Transfer codings the client accepts on this hop, including trailer fields. Hop-by-hop, and one of the fields a smuggling-aware proxy should normalise rather than forward blindly. | TE: trailers |
| Allow Response | The methods this resource supports. Required on a 405. A 405 without Allow leaves the caller guessing, which defeats the entire point of the status code. | Allow: GET, HEAD, PUT, OPTIONS |
| Location Response | Where to go next: the redirect target on a 3xx, or the URL of the thing you just created on a 201. A 201 without a Location is a half-finished API. So is a 202 that gives you nowhere to poll. | Location: /orders/1f9c2b |
| Link Response | Typed relations to other URLs: pagination, preload, canonical, API discovery. Paired with a 103 Early Hints response, rel=preload buys you asset fetches while the origin is still assembling the real answer. | Link: </orders?page=3>; rel="next", </app.css>; rel=preload; as=style |