Framing versus compression — two headers people conflate
Transfer-Encoding and Content-Encoding look similar but mean different
things. One frames the message for a single connection hop (chunked); the other
compresses the representation end-to-end (gzip, br). Confusing them leads to
double-compression bugs and caches serving undecodable bodies. This reference
sorts every coding value into the right layer.
How it works
Content-Encodingis end-to-end. The body is compressed (gzip,br,deflate,zstd) and a cache stores it that way; the client decodes it. It is negotiated viaAccept-Encodingand recorded inVary.Transfer-Encodingis hop-by-hop, applied for one connection then stripped by the next. Its dominant value ischunked, which streams a body of unknown length as size-prefixed chunks ending in a zero-length chunk:
Transfer-Encoding: chunked
7\r\n
Mozilla\r\n
0\r\n
\r\n
Multiple codings are applied left-to-right and decoded right-to-left, with
chunked always last in a Transfer-Encoding list. In HTTP/2 and HTTP/3 the
binary framing replaces all of this, so Transfer-Encoding is prohibited there.
Tips and notes
- Compress with
Content-Encoding, notTransfer-Encoding, in practice. - Never set both a
Content-LengthandTransfer-Encoding: chunked. - Don’t double-compress: a
brbody served asgzipContent-Encoding is corrupt. - Trailers ride after the final chunk; advertise them with the
TE: trailersrequest header.