Module http/wire

http/wire
Stability: unstable — may still change; see below.

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

MessageKind

Which message is being read, which is what decides where its body ends (RFC 9112 §6.3).

Variants

VariantFieldsDescription
Request

A request. One with no framing header at all ends with its headers — the client will not close first.

Response

A response other than to a HEAD. Its body is framed by Transfer-Encoding: chunked or Content-Length, is absent by STATUS (1xx, 204, 304 — §6.3 rule 1), or is delimited by the close.

HeadResponse

The response to a HEAD request: it carries no body no matter what its framing headers say (§6.3 rule 1). Only the sender of the request knows this, so it cannot be inferred from the bytes — reading a HEAD response as a plain Response waits for a Content-Length worth of body that the server will never send, which on a reused connection is a hang rather than a close-delimited answer.

Methods
clone : (MessageKind) fn(self : &MessageKind) -> MessageKind

Parameters

NameTypeNotes
selfMessageKind

Returns: MessageKind

ContentLength

What the header section says about the length of the message body.

Variants

VariantFieldsDescription
Absent

No Content-Length field line at all.

Lengthn: usize

One non-negative length (repeated field lines all agreed on it).

Invalidmsg: 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". msg names the defect.

Dechunk enum
Dechunk

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

VariantFieldsDescription
Donedata: ArrayList(u8), end: usize

Every chunk up to and including the terminating zero chunk was present. data is the concatenated chunk data with extensions and trailers dropped, and end is the index just past the body's final CRLF — the offset where the NEXT message on a keep-alive connection begins, which is the one thing a decoder that only returned the bytes could not tell its caller.

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 body_start — decoding is not incremental.

Malformedmsg: String

The framing is broken and the message is unrecoverable; msg names the defect and its offset relative to body_start.

Functions

find_header_end function
fn(data : &ArrayList(u8)) -> usize

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

NameTypeNotes
data&ArrayList(u8)

Returns: usize

fn(data : &ArrayList(u8), header_end : usize) -> ContentLength

The Content-Length framing declared by the header section (bytes [0, header_end)).

Parameters

NameTypeNotes
data&ArrayList(u8)
header_endusize

Returns: ContentLength

is_chunked function
fn(data : &ArrayList(u8), header_end : usize) -> bool

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

NameTypeNotes
data&ArrayList(u8)
header_endusize

Returns: bool

dechunk function
fn(body : &ArrayList(u8), body_start : usize) -> Dechunk

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

NameTypeNotes
body&ArrayList(u8)
body_startusize

Returns: Dechunk

fn(generic(R : Type), stream : &R, max_bytes : usize, kind : MessageKind, io : Io, where(R <: Reader)) -> Impl(Future(Result(String, HttpError), IoExn))

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

NameTypeNotes
RTypecomptime

Parameters

NameTypeNotes
stream&R
max_bytesusize
kindMessageKind
ioIo

Returns: Impl(Future(Result(String, HttpError), IoExn))

fn(generic(R : Type), stream : &R, max_bytes : usize, kind : MessageKind, carry : &Rc(RefCell(ArrayList(u8))), io : Io, where(R <: Reader)) -> Impl(Future(Result(String, HttpError), IoExn))

Type Parameters

NameTypeNotes
RTypecomptime

Parameters

NameTypeNotes
stream&R
max_bytesusize
kindMessageKind
carry&Rc(RefCell(ArrayList(u8)))
ioIo

Returns: Impl(Future(Result(String, HttpError), IoExn))