Chapter 16 / 51
Volume 3 · Protocol Development

Request bodies: incremental reads, fixed and chunked

The request body is not a slab of memory waiting for you — it is a stream. This chapter covers the xrtHttp1BodyRead state machine, and cashes in chapter 4's /echo challenge.

Overview

Positioning: the entry point of POST/PUT business. Afterwards you can read any body encoding safely and know where the defenses against oversized, malformed and slow-loris inputs each stand.

16.1 Intro

Bodies arrive three ways: fixed-length (Content-Length), chunked, and ends-at-connection-close (no length; rare and dangerous). They share one read interface — the pReq->body state machine plus xrtHttp1BodyRead. You don't care about the encoding: the machine unifies all three into "how much, and is there more".

16.2 The framing state machine

xhttp1body · framing state machine (xsbase.h) modes: NONE / FIXED (fixed-length) / CHUNKED / CLOSE / TUNNEL Read(pBody, buf, cap) │ ├── > 0 bytes read this round (may be less than requested) ├── 0 no new data this round (retry later / wait for events) └── error code malformed chunk line / conflicting length declarations, etc. done when: accumulated == declared length (FIXED) or the terminal chunk arrives (CHUNKED) trailers: up to 8 fields at the end of chunked, extracted via the machine

Key insight: the HTTP/1. parser rejects conflicting length declarations (Content-Length together with Transfer-Encoding is a protocol error) — one family among 35 fine-grained error codes; malformed input is stopped before reaching your business.

16.3 The read-loop pattern

Full read loop (the demo pattern)verified on a real machinesource: demo-single/modules/http.h (pattern excerpt)
/* Read the whole body into a dynamic buffer (small bodies).
 * Large bodies should be processed as they arrive — see the challenge. */
static bool ReadBodyAll(XS_HttpReq* pReq, xbuffer** ppOut)
{
    xbuffer* pBuf = xrtBufferCreate(...);
    char arrChunk[8192];

    for ( ;; ) {
        xhttp1bodystatus st = xrtHttp1BodyRead(
            pReq->body, arrChunk, sizeof(arrChunk), &iGot);
        if ( st == XHTTP1_BODY_ERROR ) {
            xrtBufferDestroy(pBuf);
            return false;          /* malformed input */
        }
        if ( iGot > 0 ) xrtBufferAppend(pBuf, arrChunk, iGot);
        if ( st == XHTTP1_BODY_DONE ) break;
        if ( iGot == 0 ) {
            /* nothing new this round: real code re-enters on events */
        }
    }
    *ppOut = pBuf;
    return true;
}

What just happened: reads are incremental — each call hands you "what's there now"; if data hasn't fully arrived you keep waiting for events (re-entering the callback on the same connection's later driver rounds). That's why large uploads never need the whole body in memory: process chunk by chunk as they come.

Now chapter 4's /echo can be honored: POST → read the whole body → return it verbatim with chapter 15's ReplyRaw (echoing the request's Content-Type).

16.4 Completeness probes & 413

  • body_limit (chapter 9's Custom knob): exceeding it yields 413 — the defense sits in the driver layer; your callback never even fires
  • Completeness probe: the driver offers a check that consumes nothing from the transport buffer — telling whether the body has fully arrived, so you never respond on a half-body
  • Malformed chunked: bad chunk lines, illegal trailers, oversized chunk lines are parser error codes; the connection closes with 400
  • Slow-loris: idle_timeout is the time defense; byte-dribbling connections close on expiry

16.5 Contract

  • Reading: bodies are consumed only through BodyRead; the driver cross-checks your consumption against the wire — a mismatch severs the connection.
  • Completion: before XS_OK, either finish the body or explicitly hand it back for draining — returning XS_OK half-read breaks keep-alive.
  • Memory: process large bodies as they arrive; don't default to ReadBodyAll.

Common mistakes

Returning XS_OK with the body half-read
Symptom
The next keep-alive request misparses / the driver severs the connection.
Cause
Body bytes remain unconsumed — the driver will drain them for you, but your response semantics are already incomplete (some paths fail outright). Finish reading, or explicitly give up (with a log), before returning.
Taking "read 0 this round" for "done"
Symptom
Large bodies sporadically only half-processed.
Cause
0 merely means "no new data right now"; the completion signal is the machine's DONE (or the accumulated length matching). Loop conditions must tell them apart.

Exercises

BasicHonoring /echo

Implement chapter 4's /echo: return the POST body verbatim.

Acceptance: test with 50 bytes and 50KB; Content-Type echoed.
Advancedchunked client input

Send a chunked body with curl -H "Transfer-Encoding: chunked" --data-binary and verify your read loop needs no changes.

Acceptance: both encodings produce identical results.
ChallengeStreaming hash upload

Implement /hash: compute SHA-256 while reading (the xrtSha family), return the hex digest when done — never buffering the full body.

Acceptance: constant memory on a 100MB upload; the result matches sha256sum.

Summary

PointConclusion
One state machinefixed / chunked / close — one Read interface
Incremental readsprocess per chunk; 0 = no data now ≠ done
Defensesbody_limit 413 · malformed 400 · idle_timeout for slow-loris
Consumption checkfinish or hand back the body before XS_OK
xrt further reading