flare_mlake · API · http · proto

flare.http.proto.chunked

Sans-I/O framing for Transfer-Encoding: chunked request bodies.

RFC 9112 sec 7.1. Two questions the server reactor needs answered, both pure functions over a byte buffer:

  • :func:scan_chunked_end -- has the whole body arrived yet? The reactor cannot use Content-Length to size a chunked request, so without this it has no completeness signal at all and treats the request as finished the moment the headers land.
  • :func:decode_chunked_body -- turn the framed bytes into the body the handler sees, plus any trailer fields.

Kept in the sans-I/O sublayer so both the server reactor and any future client sharing can use it without either importing the other's transport code. No sockets, no allocation beyond the output buffer.

Chunk-size lines are hex, optionally followed by ;ext=val parameters, which are skipped per RFC 9112 sec 7.1.1. Trailer fields after the terminating chunk are walked over so the end offset is correct, but not surfaced -- no inbound caller needs them yet. A size line or trailer line that contains a bare LF is malformed: an LF-splitting front end would end the line (and so the body) somewhere else.

Functions

classify_transfer_coding

def classify_transfer_coding(value: String) -> Int

Classify a Transfer-Encoding value as a request framing.

value is every Transfer-Encoding field of the message joined with , -- RFC 9110 sec 5.3 makes repeated fields and a comma list the same thing, so the decision must look at all of them, not the first or the last.

request_te_framing

def request_te_framing(buf: Span[UInt8], headers_end: Int, allow_content_length: Bool = False) -> Int

Decide Transfer-Encoding framing from the raw header block.

A byte scan rather than a HeaderMap lookup, because the reactor has to answer this before it parses and the minimal-parser path never builds a HeaderMap. It reads header lines the way the parser does -- field name anchored at the start of a line (lines end at LF, a preceding CR dropped) and followed by :, with optional SP/HTAB in between -- and it reads every Transfer-Encoding line, so it cannot disagree with the parser about which one counts.

Args
buf: Buffer holding the request head.
headers_end: Offset one past the CRLFCRLF terminator.
allow_content_length: When False (strict), a Content-Length alongside Transfer-Encoding is TE_INVALID: the two framings disagree and a front end may have used the other.

header_says_chunked

def header_says_chunked(buf: Span[UInt8], headers_end: Int) -> Bool

True when the request is framed by Transfer-Encoding: chunked.

Kept for callers that only need the yes/no answer. Anything that must also reject a malformed or unsupported coding should call :func:request_te_framing instead.

scan_chunked_end

def scan_chunked_end(buf: Span[UInt8], start: Int, max_body: Int) -> Int

Return the offset one past a complete chunked body.

Walks the chunk-size lines without copying any payload. Returns CHUNKED_INCOMPLETE when more bytes are needed, CHUNKED_MALFORMED on invalid framing or when the decoded size would exceed max_body, otherwise the absolute end offset (after the terminating chunk and its trailer section).

max_body is checked against the running decoded total, not the wire total, and is checked during the walk -- a chunked upload must not be allowed to grow the read buffer past the configured limit before anyone notices.

Stateless: every call walks from start. A caller that polls the same growing buffer should use :func:scan_chunked_resume.

scan_chunked_resume

def scan_chunked_resume(buf: Span[UInt8], mut cursor: Int, mut decoded_total: Int, max_body: Int) -> Int

scan_chunked_end that remembers how far it got.

cursor and decoded_total are advanced past every chunk that has fully arrived, so the next call starts at the first incomplete chunk instead of at the top of the body. The reactor re-polls after every read; restarting from the top made a body of many small chunks, delivered in many small segments, quadratic in its length.

Args
buf: Buffer holding the request.
cursor: On entry, where the next unscanned chunk-size line starts (headers_end for a fresh request). Advanced in place.
decoded_total: Payload bytes in the chunks before cursor. Advanced in place.
max_body: Cap on the decoded body.

decode_chunked_trailers

def decode_chunked_trailers(buf: Span[UInt8], start: Int, mut trailers: HeaderMap)

Append the trailer fields of the chunked body at start that may be kept (:func:trailer_field_allowed) to trailers. A trailer line that is not name: value with a token name raises. Assumes :func:scan_chunked_end accepted the buffer.

decode_chunked_body

def decode_chunked_body(buf: Span[UInt8], start: Int, mut out: List[UInt8]) -> Int

Append the decoded body to out; return the end offset.

Assumes :func:scan_chunked_end already accepted this buffer, so framing errors here raise rather than being reported as a status. Trailer fields are skipped; use :func:decode_chunked_trailers if the caller wants them.

Aliases

comptime CHUNKED_INCOMPLETE = Int(-1)

scan_chunked_end: more bytes needed.

comptime CHUNKED_MALFORMED = Int(-2)

scan_chunked_end: the framing is invalid; reject with 400.

comptime TE_ABSENT = Int(0)

request_te_framing: no Transfer-Encoding field; frame by Content-Length (or no body).

comptime TE_CHUNKED = Int(1)

request_te_framing: exactly chunked; frame by chunk size lines.

comptime TE_INVALID = Int(-1)

request_te_framing: the framing is ambiguous or malformed; 400.

comptime TE_UNSUPPORTED = Int(-2)

request_te_framing: a transfer coding other than chunked was applied; flare does not decode those, so 501 (RFC 9112 sec 6.1).

comptime CHUNK_LINE_MAX = Int(4096)

Longest chunk-size line (size plus extensions) or trailer line accepted. Without a cap, a peer can send extension bytes that never reach CRLF and keep a request incomplete for as long as the connection lives. The cap is on the line's content, CRLF excluded, and applies the same whether the line arrives whole or in pieces.