Module http/wire
HTTP/1.1 wire framing shared by std/http's client and server — header
delimiting, Content-Length, Transfer-Encoding: chunked decoding
(RFC 9112 §7.1) and the generic "read one message" loop. std-internal:
std/http/index.yo does not re-export it.
Framing is the security boundary in this file, not an implementation
detail. One read can deliver bytes belonging to TWO messages, so a
message is framed by what its own header section declares and the excess
is handed back to the caller rather than appended to the body — treating
it as body is request/response smuggling (RFC 9112 §11.2), and a
heisenbug, because it only appears when the peer's writes coalesce.
read_http_message_buffered is the form that keeps those leftover bytes;
read_http_message discards them, which is correct only for a connection
that is about to be closed.
Stability
unstable — std-internal, and the reuse story it was rebuilt for is only
half delivered. The leftover-carrying framing (read_http_message_buffered,
Dechunk.Done's end) landed 2026-09-10 so that connection reuse would
be safe, but nothing yet reuses a connection: std/http/client.yo is
one-shot fetch/fetch_with and HttpServer.serve handles one
connection at a time, so the only caller that drives carry across two
messages today is tests/http/wire.test.yo's scripted reader. The open
question is whether carry stays a caller-owned ArrayList(u8) or
becomes state inside the connection object a pooling client would own.
Freezing follows that pool.
Types
What the header section says about the length of the message body.
Variants
| Variant | Fields | Description |
|---|---|---|
Absent | No | |
Length | n: usize | One non-negative length (repeated field lines all agreed on it). |
Invalid | msg: String | The field IS present but its value is not a single non-negative
integer, or two field lines disagree. RFC 9112 §6.3 makes that
unrecoverable framing — never "no body". |
Outcome of decoding a chunked body prefix. Three arms rather than a
Result, because "not all here yet" is not an error on a stream: the
caller answers .Incomplete by reading more, and only .Malformed ends
the message.
Variants
| Variant | Fields | Description |
|---|---|---|
Done | data: ArrayList(u8), end: usize | Every chunk up to and including the terminating zero chunk was present.
|
Incomplete | Well-formed so far, but the terminating zero chunk (or its final CRLF)
has not arrived. Read more and decode again from the same
| |
Malformed | msg: String | The framing is broken and the message is unrecoverable; |
Functions
The index of the CR that opens the CRLFCRLF terminating the header
section, or 0 when that terminator is not in data yet — so the body
starts at find_header_end(data) + 4, and a 0 return means "keep
reading" rather than "empty headers" (a real message spells a start line
first, so the terminator can never sit at offset 0). O(n) over data;
pure, so it neither reads from the stream nor blocks.
Parameters
| Name | Type | Notes |
|---|---|---|
data | ArrayList(u8) |
Returns: usize
The Content-Length framing declared by the header section (bytes
[0, header_end)).
Parameters
| Name | Type | Notes |
|---|---|---|
data | ArrayList(u8) | |
header_end | usize |
Returns: ContentLength
True when the header section (bytes [0, header_end)) declares
Transfer-Encoding: chunked. Case-insensitive, and the question it asks
is whether the LAST listed coding is chunked: RFC 9112 §6.1 requires
chunked to be final, and repeated field lines concatenate in order, so the
last field line carries the final coding. A caller reaches for this before
find_content_length, because chunked framing wins over a
Content-Length that arrives with it.
Parameters
| Name | Type | Notes |
|---|---|---|
data | ArrayList(u8) | |
header_end | usize |
Returns: bool
Decode a Transfer-Encoding: chunked body (RFC 9112 §7.1): repeated
hex-size[;extensions]CRLF data CRLF, ended by a zero-size chunk followed
by optional trailer fields and a final CRLF. body is the whole raw
message and body_start its first body byte (find_header_end(...) + 4);
chunk extensions and trailer fields are parsed only far enough to skip
them, so a caller never sees them.
Pure and non-blocking — it decodes only the bytes already in hand, which
is why a body that is merely still arriving comes back as
Dechunk.Incomplete instead of an error. A chunk size past 0x0FFFFFFF is
rejected as .Malformed rather than wrapped.
Parameters
| Name | Type | Notes |
|---|---|---|
body | ArrayList(u8) | |
body_start | usize |
Returns: Dechunk
Read exactly one HTTP message off stream. For a connection used once:
anything the peer sent past this message is DISCARDED, which is the right
answer when the connection is about to be closed and the wrong one when it
is about to be reused — use read_http_message_buffered there.
Type Parameters
| Name | Type | Notes |
|---|---|---|
R | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
stream | R | |
max_bytes | usize | |
is_request | bool | |
io | Io |
Read one HTTP/1.1 message from stream: until Content-Length is
satisfied, until the terminating zero chunk of a chunked body, or — for a
RESPONSE only — until the peer closes (a response with neither framing
header is delimited by close, RFC 9112 §6.3 rule 8). A REQUEST with
neither is complete at the end of its headers (the client will not close
first). A chunked body is handed on DECODED (headers CRLFCRLF data), so
parse_response sees the payload while the Transfer-Encoding field line
stays visible on the message.
The three framing defects — HttpError.ResponseTooLarge past max_bytes
(0 = no ceiling), MalformedChunkedBody on broken chunk framing and
MalformedContentLength on a Content-Length that is not a single
non-negative integer — come back as the .Err, not as a throw: a SERVER
must be able to answer a bad client and keep serving, and a throw would
take the whole serve loop down with the one connection (D13: the pure
decoder returns Result; the client throws the .Err at its call site —
issues/fixed/one-malformed-request-killed-http-server-serve.md). I/O
failures on the stream itself still throw through io's exception.
The keep-alive form: carry holds whatever a previous call read PAST the
previous message on this connection, and on return holds whatever this call
read past THIS message. One list per connection, reused across its
messages; pass a fresh empty one for a connection that is used once.
This is what makes connection reuse safe. A single read can return bytes
belonging to two pipelined messages, and framing the second one by
"whatever arrived next" is request/response smuggling (RFC 9112 §11.2) —
and a heisenbug, because it only shows up when the peer's writes coalesce.
Type Parameters
| Name | Type | Notes |
|---|---|---|
R | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
stream | R | |
max_bytes | usize | |
is_request | bool | |
carry | ArrayList(u8) | |
io | Io |