Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.http.decoder

A resumable HTTP/1.x request decoder with no I/O. Feed it whatever bytes are available now, and retain the unconsumed suffix. It shares request-head, target, framing, chunk-size and trailer validation with std.http. Its stored state contains owned bytes rather than shared strings, so it can live inside an actor node.

decoder(&limits) returns Result[Decoder, HttpError]. The limits are copied; max_line or max_head below 2 returns InvalidLimit. Allocation failures are HttpError.Allocation. Header bytes and lines are bounded by those limits; body chunks are at most feed_budget (4096) bytes.

import std.http
import std.http.decoder
from std.http.decoder import Event

def main() -> Result[(), std.http.HttpError | IoError | OverflowError[i64]]:
    mut decoder = std.http.decoder.decoder(&std.http.limits())?
    wire = b"POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\n\r\nping"
    mut at = 0
    while not decoder.is_done():
        consumed, event = decoder.feed(&wire[at:])?
        at += consumed
        match event:
            case Some(Event.Head(head, _, _)):
                print(head.method)?
            case Some(Event.Body(body)):
                print(body.len())?
            case Some(Event.End):
                print("complete")?
            case _:
                pass
    decoder.finish()?
    Ok(())
POST
4
complete

This example already owns the complete request, so it can loop until End. A readiness loop must instead yield when no currently available input can advance the decoder. Bound the number of feed calls per scheduler turn too; the per-call byte budget alone does not bound an unbounded caller loop.

Progress and events

feed(source: &[u8]) reads a &[u8] view and returns Result[(consumed, Option[Event]), HttpError]. consumed counts from the start of the view, not from the start of the caller’s buffer. Advance the source cursor by exactly that count. Neither a short count nor Nothing implies EOF.

EventMeaning
Head(RequestHead, RequestTarget, Framing)Validated request metadata and body framing
Body(Bytes)Owned decoded body bytes
Trailers(list[Header])Validated trailer fields after a chunked body
EndRequest complete; no pipelined bytes have been consumed

An empty feed can emit a pending End. The returned head/trailer events own text, but the decoder does not retain that text after the call. Callers that keep events across actor callbacks must respect the actor’s storage boundary.

finish() reports explicit input EOF. It succeeds after a complete body, or before any nonblank request has begun, and reports truncated or malformed input otherwise. If body completion is pending, collect its End with an empty feed. finish does not make an incomplete request reusable.

State and reuse

MethodContract
is_started()Nonblank request input has arrived, even if the line is partial
has_head()A valid head was emitted and the decoder has not failed
is_done()End was emitted
total()Decoded body bytes consumed for this request
reset()Begin another request after End; otherwise Unfinished

Reset only when the application is ready to process pipelined input. Framing and allocation errors poison the decoder; discard it instead of resuming with a suffix whose request boundary is no longer trustworthy. Source ranges follow the ordinary byte-view rules; feed takes an already-formed view, not separate range parameters. The decoder never reads a socket, waits, or owns the caller’s input buffer.

Internal cursor arithmetic uses explicit opt-outs whose bounds come from the 4096-byte feed budget, validated CRLF lines and the copied head/body limits. Numeric length parsing keeps the shared parser’s recoverable errors. Calling from another arithmetic mode does not change the decoder’s HttpError contract.