Frontend Labs
⋯

Part XIX — HTTP in depth

Part XI covered HTTP's shape (versions, framing, multiplexing). This part covers the semantics you actually operate: framing hazards, caching, cookies, CORS, streaming, auth, and proxies.

58. Message framing: where does a request end?

58.1 The two ways to delimit a body

Content-Length: 1234              exact byte count; simple; needs the length up front
Transfer-Encoding: chunked        a series of  <hex-size>CRLF<data>CRLF  blocks,
                                  terminated by  0CRLF CRLF ; for streamed/unknown length

HTTP/1.1 rules (RFC 9112): if both are present, Transfer-Encoding wins and Content-Length must be ignored/rejected. A message with neither, on a response, is delimited by connection close (HTTP/1.0 style) — which is why Connection: close responses can't be pipelined.

58.2 Request smuggling — the framing attack

If a front proxy and a back-end disagree about where a request ends, an attacker can hide a second request inside the first.

Attacker sends ONE TCP stream containing:

  POST / HTTP/1.1
  Host: x
  Content-Length: 6
  Transfer-Encoding: chunked

  0

  GET /admin HTTP/1.1
  X: y

Front proxy honours Transfer-Encoding -> sees the request end at "0\r\n\r\n"
                                          and forwards the remainder as a NEW request
Back end honours Content-Length: 6      -> consumes 6 bytes, leaves "GET /admin"
                                          prefixed onto the NEXT victim's request

Variants are named by which side does what: CL.TE, TE.CL, TE.TE (obfuscated Transfer-Encoding headers so one side ignores it).

Defences

  • Normalise at the edge: reject any request containing both headers; reject unknown/obfuscated Transfer-Encoding values.
  • Use HTTP/2 end to end (its framing is explicit and length-prefixed) — but beware H2.CL / H2.TE downgrade smuggling when the proxy translates H2 to HTTP/1.1 downstream; the proxy must regenerate framing, never trust the client-supplied length.
  • Disable connection reuse to the back end if you can afford it (removes the victim's request from the picture).
  • Reject requests with duplicate Content-Length, or with Content-Length on a GET where your app doesn't expect one.

58.3 Other framing edge cases

Case Behaviour
HEAD response Has Content-Length but no body. Proxies must not wait for one.
204 No Content, 304 Not Modified Never have a body, regardless of headers.
1xx interim (100 Continue, 103 Early Hints) Followed by a second status line. Clients must handle multiple responses to one request.
Expect: 100-continue Client asks permission before sending a large body; server replies 100 or an error. Saves uploading a body that will be rejected.
Trailers Headers sent after a chunked body (Trailer: announces them). Used by gRPC for status.

58.4 Real-world example

A CDN in front of a Node.js origin: the CDN parsed Transfer-Encoding with a trailing space (Transfer-Encoding : chunked) as invalid and therefore absent, while the origin's parser trimmed the space and honoured it. Classic TE.TE. One crafted request poisoned the next user's response. The fix was a WAF rule rejecting any header name with whitespace before the colon, plus upgrading both parsers to RFC 9112-strict behaviour.

Try it yourself

# see chunked framing on the wire
curl -sv --raw http://httpbin.org/stream/3 2>&1 | head -30

# force HTTP/1.0 (no chunked; body ends at connection close)
curl -sv --http1.0 http://example.com 2>&1 | grep -i 'connection\|content-length'

# 100-continue in action
curl -sv -H 'Expect: 100-continue' -d @bigfile http://httpbin.org/post 2>&1 | grep '< HTTP'

Generated from real-life-example.md · 8 Oct 2026 16:36