Compression, content negotiation and media types

Accept and Accept-Encoding with quality values, gzip, Brotli and zstd, charset handling, multipart uploads, and byte-range requests.

Negotiating the representation

GET /report HTTP/1.1
Host: api.example.com
Accept: text/html, application/json;q=0.9, */*;q=0.1
Accept-Language: en-GB, en;q=0.8, fr;q=0.5
Accept-Charset: utf-8

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: en
Vary: Accept, Accept-Language
  • Quality values run from 0 to 1 and express relative preference, not an absolute ordering: text/html with no q means 1.0.
  • */* is the catch-all catchphrase of a client that does not care; it must rank lowest or a specific type is never chosen.
  • The server picks one representation and must say which with Content-Type. A 406 Not Acceptable is legal but rarely useful — return something sensible instead.
  • Vary tells caches which request headers produced this response. Omit it and a cached French page is served to an English client.

Compression in practice

GET /app.js HTTP/1.1
Accept-Encoding: br, gzip, zstd;q=0.9, identity;q=0.1

HTTP/1.1 200 OK
Content-Type: application/javascript; charset=utf-8
Content-Encoding: br
Vary: Accept-Encoding
Content-Length: 41230
EncodingRatio on textCostNotes
gzipGoodLowUniversal support; the safe default
brBetterHigher CPU on slow dynamic contentBrowser support is broad; pre-compress static assets
zstdComparable to BrotliFastNeeds explicit client support; good for APIs and internal traffic
identityNoneNoneAlways acceptable unless you forbid it
# pre-compress static assets at build time, then serve by name
brotli -q 11 -k app.js        # produces app.js.br
gzip -9 -k app.js             # produces app.js.gz

# verify what a server actually sent
curl -sS -o /dev/null -H 'Accept-Encoding: br,gzip' -w '%{size_download} %{content_type}\n' https://example.com/app.js
curl -sSI -H 'Accept-Encoding: br,gzip' https://example.com/app.js | grep -iE 'content-encoding|vary'
  • Never compress what is already compressed: images, video, and archives gain a few bytes at best and burn CPU.
  • Compression is a confidentiality problem too: compressing a response that mixes a secret with attacker-controlled input enables BREACH-style attacks. Disable compression on endpoints that reflect tokens.
  • Decompressing is cheap for the client but not free — very small bodies (under about a kilobyte) are often better sent uncompressed.
  • A proxy that strips Accept-Encoding must also strip Content-Encoding, or the client receives bytes it cannot decode.

Charsets, multipart and ranges

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----XyZ123
Content-Length: 481

------XyZ123
Content-Disposition: form-data; name="title"

Q3 report
------XyZ123
Content-Disposition: form-data; name="file"; filename="q3.png"
Content-Type: image/png

<binary bytes>
------XyZ123--
GET /video.mp4 HTTP/1.1
Range: bytes=0-1048575

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 0-1048575/10485760
Content-Length: 1048576
SituationResponseWhy
Range satisfied206 with Content-RangeVideo seeking and resumable downloads
Range outside the file416 with Content-Range: bytes */sizeTells the client the real size
Range header ignored200 with the whole bodyLegal — servers may ignore ranges
Conditional range206 only if If-Range matchesStops a resumed download appending bytes from a different version
⚠️
Forgetting Vary: Accept-Encoding is the classic compression bug: a CDN caches the Brotli version and then serves it to a client that never asked for it, producing a corrupted page with no obvious error. Send it on every response whose encoding depends on the request.

FAQ

Why is my JSON still uncompressed?
Either the client did not send Accept-Encoding, the server's compression middleware excludes that content type, or the body is below the minimum size threshold. Check the response headers before changing code.
Does <code>charset</code> matter in <code>Content-Type</code>?
Yes for text media types. Without it, a browser or library may fall back to a locale default and mangle non-ASCII characters. Send charset=utf-8 and be consistent end to end.

Message anatomy and framing Proxies, load balancers and connection management

Last refreshed 2026-09-18.