Skip to content
Reading the Request Body
step 1/5

Reading — step 1 of 5

Read

~3 min readConnection & Parsing

Reading the Request Body

Headers end at the blank line — and then how many bytes of body follow? TCP won't tell you: it's a stream, not a message service, with no "end of request" marker. HTTP solves message framing with two mechanisms, and the second one — chunked encoding, your exercise — is a tiny protocol of its own.

Mechanism 1: Content-Length

The simple case: the sender counts the body's bytes and declares them.

POST /api/notes HTTP/1.1\r\n
Content-Length: 27\r\n
\r\n
{"title":"hello","id":123}

The parser reads the headers, sees 27, then reads exactly 27 bytes — no more (the next bytes may be the next request on a keep-alive connection! over-reading corrupts the stream), no fewer (under-reading leaves body bytes to be mis-parsed as the next request line). Both failure modes are real and nasty; exactness is the contract. Validation applies as always: non-numeric or negative Content-Length is a 400; two different Content-Lengths (last lesson) likewise.

Mechanism 2: chunked — framing without foreknowledge

Now the interesting case: the sender doesn't know the total size — it's streaming (generating output as it goes, proxying, compressing on the fly). Enter Transfer-Encoding: chunked, which frames the body as a sequence of self-describing pieces:

7\r\n
Mozilla\r\n
9\r\n
developer\r\n
0\r\n
\r\n

Grammar per chunk: size in HEX, CRLF, exactly that many bytes of data, CRLF. Repeat. A chunk of size 0 marks the end (followed by a final CRLF — after optional "trailers," which your spec will likely have you skip). Decoded, the body above is Mozilladeveloper — 16 bytes the sender never had to pre-count. The parser is a clean loop:

loop:
    read line → parse as hex size          (bad hex? 400)
    if size == 0: consume trailing CRLF, done
    read exactly size bytes                (the DATA may contain \r\n freely —
    expect CRLF                             you read by COUNT, not by lines!)

That parenthetical is the classic bug: chunk data is length-delimited, not line-delimited — a chunk can contain CRLFs, and readers that switch to line-reading inside data corrupt everything. Sizes are lines; data is counted bytes; the trailing CRLF after data is a frame separator, not part of the data.

Second classic: the size is hexadecimal. 1A means 26. Parsers that read it as decimal work on small test bodies (where sizes stay under 10) and explode on real ones — a beautifully evil bug, and yes, the graders include a hex-letter size.

Rule of precedence you should also encode: if Transfer-Encoding: chunked is present, it wins over any Content-Length (the spec says ignore CL; security folk say a request carrying both deserves suspicion — it's the request-smuggling primitive — and modern servers often just 400 it).

Your exercise: Decode Chunked Bodies

Chunked streams in; decoded bodies (or 400s) out. The ladder: single chunk → multiple chunks → hex sizes with letters → data containing CRLF → empty body (just the 0 chunk) → malformed size → missing terminator. It's ~20 lines that force the deepest habit of protocol work: read by the grammar, not by what looks like lines — the same discipline as the git packfile parser and every binary format you'll ever meet, here in text where you can still debug by eyeball.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…