moonhttp
The HTTP family for MoonBit: the formats a request and a response are written in, each a package of its own. Bytes in, events out; no sockets.
moon add moonbitstack/moonhttp@form
A request body read as the form its Content-Type says it is: multipart/form-data through moonmime, application/x-www-form-urlencoded through moonurl.
fn parse( body : BytesView, content_type : StringView, limits~ : @moonmime.Limits = @moonmime.limits, strict~ : Bool = false) -> @moonmime.Form raise @moonmime.Refused
Read a request body as the form its Content-Type says it is: Go's Request.ParseForm, and Starlette's Request.form(), without the request. Any other content type, or a multipart one with no boundary to split on, carried no form, and the answer is an empty one; strict raises NotAForm instead. limits bounds both encodings, its parts counting pairs in a urlencoded body.
@range
RFC 9110 §14 range requests: the fields, the arithmetic against a length, and the multi-range body.
let unit : String = "bytes"
The only range unit HTTP defines (RFC 9110 §14.1.2). It is a parameter everywhere rather than a constant because §14.1 leaves the registry open, and a reader that hard-coded bytes would silently mis-handle a unit it was never told about instead of ignoring it.
let limit : Int32 = 200
How many ranges one request may ask for before the answer is to send the whole thing instead (RFC 9110 §14.2). A request may name any number of ranges, and a server that honours all of them does a great deal of seeking for one client — the shape of the 2011 Apache range attack. 200 is Apache's own MaxRanges default.
enum Spec { /// `first-last`: both positions, inclusive. Between(Int64, Int64) /// `first-`: from here to the end, however long it turns out to be. From(Int64) /// `-suffix`: the last so many octets. Last(Int64) } derive(Eq, Debug)
One range as a client asked for it, before it is measured against a length (RFC 9110 §14.1.1). The three shapes are the grammar's: an int-range with both ends, an int-range with only a first position, and a suffix-range. They stay separate because they mean different things — From(0) asks for everything, while Last(0) asks for nothing and is unsatisfiable.
struct Part { at : Int64 last : Int64 } derive(Eq, Compare, Debug)
One range measured against a representation: the first and last octet, both inclusive (RFC 9110 §14.1.2). Inclusive because the field is, and converting at the edge is where off-by-one errors come from. [Part::length] gives the count for a caller who wants one.
fn Part::length(self : Part) -> Int64
How many octets this part covers.
fn decode(value : StringView, unit~ : String = unit) -> Array[Spec]?
Read a Range field value (RFC 9110 §14.2). None means the field is to be ignored, which §14.2 requires for a unit the server does not implement and for a value that does not parse. Ignoring it is not an error: the answer is the whole representation, with status 200. An empty range-set is not a range request either — bytes= names no range, and serving everything is the right reading of it.
fn encode(specs : ArrayView[Spec], unit~ : String = unit) -> String
Write a Range field value (RFC 9110 §14.2).
fn measure( specs : ArrayView[Spec], length~ : Int64, merge~ : Bool = true, limit~ : Int32 = limit) -> Array[Part]?
Measure what was asked against a representation of length octets (RFC 9110 §14.1.2). None is unsatisfiable, and the answer to it is 416 with the [unsatisfied] Content-Range — not 200, and not an empty 206. A request is unsatisfiable only when *no* spec in it is satisfiable; §14.1.1 says one good spec carries the field, and the ones that cannot be met are dropped. merge coalesces ranges that overlap or touch, which §14.2 permits and which keeps bytes=0-100,50-150 from sending the middle twice. limit caps how many ranges survive; past it the caller should send the whole representation, so exceeding it reads as "no ranges here" rather than as an error.
struct Content { unit : String part : Part? complete : Int64? } derive(Eq, Debug)
A Content-Range field value (RFC 9110 §14.4): where a part sits in the whole, or — in a 416 — only how big the whole is. part is absent in the */complete form a 416 carries. complete is absent in the part/* form, which says the total length was not known when the field was written.
fn Content::new( part : Part, complete? : Int64, unit~ : String = unit) -> Content
The Content-Range of a part that was served (RFC 9110 §14.4). complete is the length of the whole representation. Leaving it out writes *, which §14.4 reserves for a sender that does not know the total — a generated response being streamed, say. Give it when it is known: a client recombining parts needs it.
fn unsatisfied(complete : Int64, unit~ : String = unit) -> Content
The Content-Range a 416 carries: bytes */1234 (RFC 9110 §14.4). It names no part because there is none to name; the number is what the client needs in order to ask again for something that exists.
fn Content::encode(self : Content) -> String
Write a Content-Range field value (RFC 9110 §14.4).
fn Content::decode(value : StringView) -> Content?
Read a Content-Range field value (RFC 9110 §14.4). None is a value that cannot be trusted, and §14.4 is explicit about what follows: a recipient must not recombine content with a stored representation on the strength of an invalid Content-Range. So a last position before the first, or a complete length that does not reach past the last position, is refused here rather than repaired.
fn accept(ranges~ : Bool = true, unit~ : String = unit) -> String
The Accept-Ranges field value (RFC 9110 §14.3). none is not the absence of the field: it says outright that range requests will not be honoured, which stops a client from trying and failing.
fn multipart(boundary : StringView) -> String
The Content-Type a multi-range 206 carries (RFC 9110 §14.6).
fn parts( parts : ArrayView[(Part, Bytes)], boundary~ : String, kind~ : String, complete? : Int64, unit~ : String = unit) -> Bytes
The body of a multi-range 206 (RFC 9110 §14.6): one part per range, each with its own Content-Type and Content-Range, then a closing boundary. parts pairs each range with its octets — this package does no reading, so the caller fetches them. kind is the representation's own media type, which §14.6 repeats in every part because each part is a document in its own right.
@conditional
RFC 9110 §8.8 and §13: entity-tags, the five precondition fields, and the order §13.2.2 weighs them in.
struct Tag { text : String weak : Bool } derive(Eq, Debug)
An entity-tag (RFC 9110 §8.8.3). text is what is inside the quotes; keeping them would put the field's syntax inside the value. weak is the W/ in front. Two weak tags mark representations that are equivalent, not identical — enough for "has this changed", not enough for "may I splice these two ranges".
enum Tags { Any These(Array[Tag]) } derive(Eq, Debug)
What a precondition field names (§13.1.1): every representation, or these tags. * is not a tag that matches everything. §13.1.1 and §13.1.2 both read it as "does a representation exist", and answer in opposite directions.
enum Validator { Marked(Tag) Dated(@moondate.Moment) } derive(Eq, Debug)
Either kind of validator (§8.8). If-Range takes one or the other (§13.1.5); the other four fields each take one kind.
enum Verdict { /// No precondition failed: carry on with the method. Go /// The client's copy is still good — 304, with no body (§15.4.5). Fresh /// A precondition failed — 412 (§15.5.13). Failed } derive(Eq, Debug)
What §13.2.2 says to do with the request once the preconditions are weighed.
fn Tag::new(text : String, weak~ : Bool = false) -> Tag
A tag with this text.
fn Tag::same(self : Tag, other : Tag) -> Bool
Strong comparison (§8.8.3.2): neither is weak and the texts match. If-Match and If-Range use this one — both are about replacing or splicing octets, which a weak tag does not promise.
fn Tag::alike(self : Tag, other : Tag) -> Bool
Weak comparison (§8.8.3.2): the texts match, weakness ignored. If-None-Match uses this one — a weak tag can answer "has it changed".
fn hash( raw : BytesView, digest~ : &@spec.Hash, weak~ : Bool = false) -> Tag
An entity-tag from the octets themselves: their count and their digest, both in hexadecimal. Strong, which is the point of hashing the content. digest is a parameter — which hash to spend on every response is the server's trade. The shape is the etag package Express serves static files with, "<length>-<digest>", in nginx and Apache's hexadecimal.
fn stamp( length~ : Int64, modified~ : @moondate.Moment, weak~ : Bool = true) -> Tag
An entity-tag from what a file system knows without opening the file: when it last changed and how long it is. nginx's "<mtime>-<length>", one stat. Weak, as a one-second timestamp must be: two writes inside the same second leave both numbers unchanged.
fn Tag::encode(self : Tag) -> String
Write the tag as a field value: "x", or W/"x" (§8.8.3).
fn Tag::decode(text : StringView) -> Tag?
Read one entity-tag. None when it is not one — usually an unquoted value, which §8.8.3 has no room for.
fn Tags::encode(self : Tags) -> String
Write a whole field value: *, or the tags separated by commas.
fn Tags::decode(text : StringView) -> Tags?
Read a whole field value. Commas are found outside the quotes: , is in the etagc set (§8.8.3), so a tag may contain one. None when any member is not an entity-tag — §13.1.1 has no reading for half a list.
fn Validator::decode(text : StringView) -> Validator?
Read an If-Range value (§13.1.5). The first two characters tell them apart, as §13.1.5 says: only an entity-tag starts with a quote, only a weak one with W/.
fn Validator::encode(self : Validator) -> String raise @moondate.Refused
Write an If-Range value. Raises for a moment with no zone — an HTTP-date is GMT by definition (§5.6.7).
struct Ask { /// `If-Match` (§13.1.1). matching : Tags? /// `If-None-Match` (§13.1.2). none_matching : Tags? /// `If-Modified-Since` (§13.1.3). modified_since : @moondate.Moment? /// `If-Unmodified-Since` (§13.1.4). unmodified_since : @moondate.Moment? /// `If-Range` (§13.1.5). ranging : Validator? } derive(Eq, Debug)
The five precondition fields of one request (§13.1), already read.
let ask : Ask =
A request that carried no preconditions at all.
fn Ask::new( matching? : Tags, none_matching? : Tags, modified_since? : @moondate.Moment, unmodified_since? : @moondate.Moment, ranging? : Validator) -> Ask
The preconditions a request carried, named one at a time.
fn Ask::read(headers : ArrayView[(String, String)]) -> Ask
Pull the five fields out of a request's headers. Names match without regard to case (§5.1). Two lines of one list-valued field are one list (§5.3), so the tag fields are joined before reading; the date fields are not lists, so a repeat is ignored. A field present but unreadable comes back absent, per §13.1.3 and §13.1.4.
fn Ask::evaluate( self : Ask, verb~ : String, etag? : Tag, modified? : @moondate.Moment, present~ : Bool = true) -> Verdict
Weigh the preconditions in the order §13.2.2 sets out. verb is the request method (method is reserved). It decides what a failed If-None-Match means: 304 for a GET or HEAD, 412 otherwise. No default — guessing GET would tell a PUT its copy is current. etag and modified are what the server holds; present is whether it holds anything at all, which is all * asks.
fn Ask::unchanged( self : Ask, etag? : Tag, modified? : @moondate.Moment) -> Bool
Whether If-Range still holds, and so whether a Range may be honoured (§13.1.5). Absent holds: no condition, nothing in the way. Both kinds compare strongly, and a weak tag is refused rather than compared — "may these octets be spliced onto yours" is what a weak tag may not answer.
@cookie
RFC 6265 Cookie and Set-Cookie, each read and written.
enum SameSite { Strict Lax Unrestricted } derive(Eq, Debug)
How far a cross-site request may carry a cookie. Strict sends it only on a same-site request, Lax also on a top-level navigation, and Unrestricted sends it everywhere. Unrestricted is the wire value None, spelled differently here because None is the empty option; a browser honours it only on a Secure cookie.
fn SameSite::code(self : SameSite) -> String
The attribute's wire spelling.
fn SameSite::of(text : StringView) -> SameSite?
The attribute a spelling names, or None for one no revision defines. The comparison ignores case, as §5.2 says to do with every attribute value.
struct Cookie { name : String value : String /// When the cookie dies, written as the HTTP-date `Expires` carries. expires : @moondate.Moment? /// How long it lives, in seconds. A browser that understands it prefers it to /// `Expires`, which is why a deletion writes both. max_age : Int32? domain : String? path : String? secure : Bool http_only : Bool same_site : SameSite? } derive(Eq, Debug)
One cookie, as Set-Cookie states it. An attribute left None is left out of the header, and means what leaving it out means: no Expires and no Max-Age is a session cookie, no Domain is the origin host alone, no Path is the request's own directory.
fn Cookie::new( name : String, value : String, expires? : @moondate.Moment, max_age? : Int32, domain? : String, path~ : String? = Some("/"), secure~ : Bool = false, http_only~ : Bool = false, same_site~ : SameSite? = Some(Lax)) -> Cookie
A cookie by name, with the defaults a server usually wants. path defaults to /, so the cookie covers the whole site, and same_site to Lax, which is what a browser assumes of a cookie that does not say — and what stops a cross-site form post from carrying a session. Passing None for either leaves the attribute out.
fn Cookie::encode(self : Cookie) -> String
The Set-Cookie field value this cookie is sent as. Attributes come in RFC 6265 §4.1.1's order, which is the order a browser and a log reader expect. The octets a name, value, domain or path may not carry are dropped rather than escaped: what a reader gets back is then the text that was set, and a \r\n smuggled into a value cannot open a header of its own. An expires that is a time with no date aborts: a cookie cannot die at a clock reading, and a caller that built one has made a mistake rather than met a condition.
fn Cookie::decode(raw : StringView) -> Cookie?
The cookie a Set-Cookie field value states, or None when it states none — an empty field, or one whose first pair has no =. Everything §5.2 says to ignore is ignored rather than refused: an attribute no revision defines, an Expires that is not a date, a Max-Age that is not a number. A server sends what it sends, and a client that refused the header would lose the cookie over an attribute it does not even use.
fn encode(items : ArrayView[(String, String)]) -> String
The Cookie field value that sends these pairs, which is what a client writes back from what it was set. The header carries names and values and no attributes at all: those were the server's instructions to the client, not something the client repeats.
fn decode(raw : StringView) -> Array[(String, String)]
The pairs a Cookie field value carries, in the order they arrived. A piece with no = is dropped rather than read as a name with no value, which is what §5.4 leaves a server free to do and what every server does. Duplicate names are kept: which one wins is the reader's decision, and [get] makes the one browsers make.
fn get(raw : StringView, name : StringView) -> String?
The value of the cookie named name in a Cookie field value, or None. The first of a repeated name wins, which is the value a browser sends first and the one every server library reads.
@sse
WHATWG Server-Sent Events, framed both ways and dispatched the way an EventSource does.
struct Event { data : String /// The type an `addEventListener` binds to. Absent means `message`. kind : String? /// Sets the client's `lastEventId`, which it sends back on reconnection. id : String? /// How long the client should wait before reconnecting, in milliseconds. retry : Int32? /// A `:`-prefixed line. It dispatches nothing and exists to keep a connection /// warm through a proxy that would otherwise close it. comment : String? } derive(Eq, Debug)
One event on the wire. A field left None is left out of the frame. data carrying newlines goes out as several data: lines and comes back joined, which is the format's own rule and the reason a payload with a blank line in it cannot be sent as one.
fn Event::of(data : String) -> Event
An event carrying only data, which is the common case.
fn Event::new( data~ : String = "", kind? : String, id? : String, retry? : Int32, comment? : String) -> Event
An event with whichever fields are wanted.
fn Event::ping(comment~ : String = "") -> Event
A comment-only frame: nothing is dispatched and the connection stays warm.
fn Event::encode(self : Event, space~ : Bool = true) -> String
The frame this event is sent as, ending in the blank line that dispatches it. space writes the one optional space after each colon, which every stream in the wild carries and which a reader strips again. Turning it off saves a byte per field on a stream where that matters, and changes nothing a client sees.
fn decode(input : StringView) -> Array[Event]
Read a stream of frames. Anything the format does not define is ignored rather than refused — an unknown field name, a line with no colon, a stray carriage return. That is what the specification says to do, and it is what lets the format grow. A trailing partial frame, one not yet closed by a blank line, is left out: it has not been dispatched and is not an event yet.
fn events(input : StringView) -> Array[Event]
The events an EventSource dispatches from a stream: what a browser hands its listeners, where [decode] answers the frames as they were written. Three rules differ, all from the specification's "Interpreting an event stream". A frame with no data: line dispatches nothing, so a comment or a lone retry: is not an event. The type is always set, message when the frame named none or named the empty string. And id and retry are the stream's, not the frame's: each holds the last value the stream set until a later line changes it, which is what a client sends back as Last-Event-ID and waits before reconnecting. A trailing frame not yet closed by a blank line is left out, as in decode.
let media_type : String = "text/event-stream"
The media type an event stream is served as.
@ws
RFC 6455 §5 WebSocket framing: opcodes, masking, reassembly, close statuses.
suberror Refused { /// The peer broke the framing: a set RSV bit with no extension negotiated, a reserved /// opcode, a client frame that is not masked, a control frame that is fragmented or /// longer than 125 bytes, a continuation with no message in progress, or a new data /// frame before the last one finished. Protocol(String) /// A text message or close reason whose bytes are not UTF-8 (§8.1), or a close code /// §7.4.1 does not allow on the wire. Payload(String) /// The peer announced or sent more than this endpoint accepts. Exceeded(limit~ : Int32, got~ : Int64) } derive(Eq, Debug)
A protocol violation, carrying the status the connection must then be failed with (RFC 6455 §7.4.1, §7.1.7). This is not end of stream: a peer that simply stops sending has not violated anything, and the connection closes without a status.
fn Refused::status(self : Refused) -> Int32
The §7.4.1 status a violation is closed with: 1002 for framing, 1007 for payload, 1009 for size.
let limit : Int32 = 16 * 1024 * 1024
How many bytes of payload this endpoint accepts, in one frame and in a message reassembled from many. Sixteen mebibytes is uvicorn's --ws-max-size. The ceiling has to bound the reassembled message and not only each fragment, because a message split into enough small frames would otherwise pass every per-frame check and still exhaust memory.
enum Opcode { Continuation Text Binary Close Ping Pong } derive(Eq, Debug)
A frame opcode (RFC 6455 §5.2): the three data opcodes and the three control ones.
fn Opcode::code(self : Opcode) -> Int32
The four-bit value on the wire.
fn Opcode::of(code : Int32) -> Opcode?
The opcode a wire value names, or None when §5.2 reserves it.
fn Opcode::is_control(self : Opcode) -> Bool
Whether this is a control opcode, which §5.5 holds to tighter rules than a data frame: at most 125 bytes of payload, and never fragmented.
struct Frame { fin : Bool opcode : Opcode payload : Bytes } derive(Eq, Debug)
A frame (RFC 6455 §5.2): its FIN bit, its opcode, and its payload already unmasked.
fn Frame::encode(self : Frame, mask~ : Bytes = b"") -> Bytes
A frame on the wire (RFC 6455 §5.2). A four-byte mask sets the MASK bit and masks the payload. A client must mask every frame and a server must mask none (§5.1), and the key has to be unpredictable, so it comes from the caller: this package has no randomness and should not pretend to. The length takes the shortest of the three forms — seven bits up to 125, sixteen bits under 126, sixty-four under 127.
fn check( b0 : Int32, b1 : Int32, len : Int64, client~ : Bool, limit~ : Int32 = limit) -> Opcode raise Refused
Check a frame's first two bytes and its announced length, answering with the opcode. client says the frames being read came from a client, whose frames must be masked (§5.1); a server's must not be, and reading those takes client=false. Run this before reading the payload. It is the length check that keeps a sixty-four bit length from being truncated into a short allocation, and keeps an honest but enormous one from being allocated at all — the sign bit included, which §5.2 reserves.
fn decode( b : BytesView, client~ : Bool = true, limit~ : Int32 = limit) -> (Frame, Int32)? raise Refused
One frame at the start of b, with its payload unmasked, and how many bytes it took. None means b does not hold a whole frame yet, so a caller feeds it more and asks again. A frame that breaks the rules is refused rather than answered None, so a violation is never mistaken for a short buffer.
enum Message { Text(String) Binary(Bytes) Close(status~ : Int32, reason~ : Bytes) } derive(Eq, Debug)
A complete application message: text with its payload decoded, binary with its bytes, or a close with its status and reason. Control frames do not surface here — a Reader answers a ping itself — so an application sees what it sent and received and nothing of the protocol's upkeep. Text carries a String rather than bytes because §8.1 makes invalid UTF-8 a 1007 failure: bytes that would not decode never get this far, so nothing hands an application a message the peer did not send.
fn close(status : Int32, reason~ : String = "") -> Bytes
A close frame's payload (RFC 6455 §5.5.1): the status big-endian, then the reason.
fn status(payload : BytesView) -> Int32
The status a close payload carries, or 1005 when it carries none (§7.1.5). 1005 is a local sentinel and must never be sent: a close that came with no status is echoed with no status, not with this number.
fn reason(payload : BytesView) -> Bytes
The reason a close payload carries — everything after the two status bytes.
fn allowed(status : Int32) -> Bool
Whether a status may appear on the wire (RFC 6455 §7.4.1 and the IANA registry). 1004 is reserved, and 1005, 1006 and 1015 are sentinels an endpoint sets locally — a peer that sends one has broken the protocol. 1012 through 1014 were registered after RFC 6455 and are allowed here, because refusing a registered status would break a real deployment for the sake of a closed reading of §7.4.1.
struct Step { message : Message? reply : Frame? } derive(Eq, Debug)
What a frame made of the connection: a message when it finished one, and a frame to send back when the protocol owes one. A ping owes a pong, and a close owes a close (§5.5.1). The reply comes back as a frame rather than as bytes because a client has to mask what it sends and only the caller has the randomness to do it with.
struct Reader { payload : Buffer mut kind : Opcode? limit : Int32 }
The state of one direction: what has been assembled so far, and of what kind.
fn Reader::new(limit~ : Int32 = limit) -> Reader
A reader with nothing assembled.
fn Reader::pending(self : Reader) -> Int32
How many bytes of the message in progress have been assembled.
fn Reader::feed(self : Reader, frame : Frame) -> Step raise Refused
Take one frame (RFC 6455 §5.4 for fragments, §5.5 for the control frames interleaved with them). A ping is answered with a pong carrying the same payload, a pong is dropped, and a close is echoed and then reported. A data frame extends or finishes the message.
@upgrade
RFC 6455 §4, the opening handshake — both sides, and the proof that makes the difference.
let guid : String = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
The string appended to a client's key before hashing (RFC 6455 §4.2.2). It is there so that a server which merely echoes what it was sent cannot pass for one that understood the protocol — a cached HTTP proxy, say.
fn accept(key : String) -> String
The Sec-WebSocket-Accept for a client's Sec-WebSocket-Key: base64 of the SHA-1 of the key with the GUID appended (RFC 6455 §4.2.2).
fn key(random : BytesView) -> String
The Sec-WebSocket-Key for sixteen random bytes (RFC 6455 §4.1). The randomness is the caller's: the key has to be unpredictable to the server, and a library with no entropy of its own should not invent any. A key of another length is what the caller asked for and is encoded as given.
fn subprotocol( offered : ArrayView[String], supported : ArrayView[String]) -> String?
The first subprotocol the client offered that this endpoint supports, or None (RFC 6455 §4.2.2). The client's order is its preference, so it is the one that decides.
fn request( host~ : String, key~ : String, path~ : String = "/", protocols~ : ArrayView[String] = [][:], origin? : String, extra~ : ArrayView[(String, String)] = [][:]) -> Bytes
A client's opening handshake (RFC 6455 §4.1): the GET that asks for the upgrade. key comes from key, over bytes the caller drew. protocols and origin are written only when given, because §4.1 makes both optional and a server may treat an empty list as a request for a subprotocol it cannot provide.
fn response( key : String, protocol? : String, extra~ : ArrayView[(String, String)] = [][:]) -> Bytes
The 101 Switching Protocols that completes the handshake (RFC 6455 §4.2.2): the upgrade headers, the proof, and the one subprotocol the server selected.
suberror Refused { /// `Upgrade` or `Connection` does not ask for a WebSocket (§4.2.1). NotAnUpgrade(String) /// A version other than 13, which §4.4 answers with `Sec-WebSocket-Version: 13`. Version(got~ : String) /// `Sec-WebSocket-Key` is missing, or not sixteen bytes of base64 (§4.2.1). Key(String) /// The server's `Sec-WebSocket-Accept` is not the proof for the key that was sent. Proof(want~ : String, got~ : String) } derive(Eq, Debug)
Why a handshake was not accepted, so a server can answer with the right status and a client can say what the server got wrong.
fn read_request( headers : ArrayView[(String, String)]) -> (String, Array[String]) raise Refused
Read a client's handshake headers and answer with its key and the subprotocols it offered (RFC 6455 §4.2.1). Header names are matched without regard to case, and Connection is read as the comma-separated list §4.2.1 allows it to be, because a proxy may have added to it. A version other than 13 is refused, which is what §4.4 asks a server to answer.
fn read_response( key : String, headers : ArrayView[(String, String)]) -> Unit raise Refused
Check a server's response headers against the key that was sent (RFC 6455 §4.1). A client that skips this accepts an upgrade from anything that can write 101, which is the case §4.2.2's proof exists to rule out.
@header
RFC 9110 §5 header fields, as the octets both header compressions share.
struct Header { name : Bytes value : Bytes } derive(Eq, Debug)
One header field: a name and a value, as octets (RFC 9110 §5). Octets and not strings, for two reasons. A field value is a byte sequence that a receiver only later decides how to read, and gRPC's -bin metadata is binary by design; decoding either to a String on the way past would lose what was sent. Both header compressions produce and consume these, so HTTP/2 and HTTP/3 speak of a header list in the same type and nothing has to be converted between them.
fn Header::of(name : String, value : String) -> Header
A field from ASCII, which is how a specification writes one and how most code means one. A code point past 0x7f is written as its low octet, so this is for names and values that are ASCII to begin with.
fn Header::ascii(self : Header) -> (String, String)
The field's name and value read back as ASCII. Every octet becomes one code point, so this round-trips what of wrote and turns arbitrary bytes into something printable rather than refusing them; a value that is meant to be UTF-8 is the caller's to decode.
fn Header::cost(self : Header, overhead~ : Int32 = 32) -> Int32
How many octets the field costs a compressor's table: its two parts and a fixed overhead for the entry itself (RFC 7541 §4.1, and RFC 9204 §3.2.1 with the same arithmetic).
@huffman
RFC 7541 Appendix B, the Huffman code HPACK and QPACK both use.
suberror Refused { Eos Padding(String) Malformed(String) } derive(Eq, Debug)
Why a Huffman string was refused: the EOS symbol appeared in the input, the padding is not a run of fewer than eight one-bits, or the bits leave the code space (RFC 7541 §5.2). All three are what §5.2 calls a decoding error, and a receiver treats them the same way; they are separate here so a log says which.
fn encode(input : BytesView) -> Bytes
Huffman-encode input (RFC 7541 §5.2): each octet becomes its code, and the final partial octet is padded with the most-significant bits of the EOS code (all ones). The inverse of decode for valid inputs.
fn size(input : BytesView) -> Int32
The number of octets input occupies when Huffman-encoded, without building the output — used to choose the shorter of raw vs. Huffman string literals.
fn decode(input : BytesView) -> Bytes raise Refused
Huffman-decode input (RFC 7541 §5.2). Raises HuffmanError if the input contains the EOS symbol, if the trailing padding is not a run of fewer than 8 one-bits, or if the bit stream leaves the code space. The inverse of encode.
@hpack
RFC 7541 HTTP/2 header compression: both tables, both ends.
suberror Refused { Malformed(String) Index(got~ : Int32) Exceeded(limit~ : Int32, got~ : Int32) } derive(Eq, Debug)
Why a header block was refused (RFC 7541 §6). Malformed and Index are connection errors in HTTP/2 (RFC 9113 §4.3): the two ends' dynamic tables have gone out of step and nothing after this point can be read. Exceeded is not. The block was decoded to its end, so the table is in step, and only this one field section is refused for being larger than the caller allows.
let limit : Int32 = 4096
How many octets a dynamic table holds by default. Four kibibytes is the value HTTP/2 gives SETTINGS_HEADER_TABLE_SIZE before either end has said otherwise (RFC 9113 §6.5.2), so it is what both tables start at.
fn entry(index : Int32) -> @header.Header?
Look up an HPACK static-table entry by its 1-based RFC index (1..=61), returning (name, value) or None when the index is out of range.
fn string(octets : BytesView, huffman~ : Bool = true) -> Bytes
A string literal (RFC 7541 §5.2): the length as a seven-bit-prefix integer, then the octets, with the H bit saying whether they are Huffman-coded. huffman takes the shorter of the two forms, which is what an encoder does; false writes the octets as they are, which is what a test with a published block needs. A tie goes to the coded form, which is what RFC 7541's own examples do: it costs nothing and leaves less of the value legible to anything reading the connection.
fn read_literal( data : BytesView, offset : Int32) -> (Bytes, Int32) raise Refused
Decode an HPACK string literal from data at offset, returning (octets, bytes_consumed). The length is read as a 7-bit-prefix integer (the H bit is masked off); this is the inverse of string for H = 0. Huffman decoding is not applied here, so ask @prefix.flagged first if the literal may be coded.
let static_table : Array[@header.Header] =
The static table as (name, value) octet pairs, in RFC index order (index 1 at position 0). Derived once from static_table.
fn read_string( data : BytesView, offset : Int32) -> (Bytes, Int32) raise Refused
Read an HPACK string literal at offset, resolving Huffman coding when the H bit is set, returning (octets, bytes_consumed). Unlike read_literal, this applies Huffman decoding. Raises on bad Huffman.
struct Table { mut entries : Array[(Bytes, Bytes)] mut size : Int32 mut limit : Int32 }
The HPACK dynamic table: a FIFO of recently seen (name, value) entries, newest first (entries[0]), bounded by limit octets where each entry costs name.len + value.len + 32 (RFC 7541 §4.1). Adding evicts the oldest entries until the newcomer fits; an entry larger than limit empties the table and is not stored (§4.4).
fn Table::new(limit~ : Int32 = limit) -> Table
A new empty dynamic table bounded by limit octets (default 4096, the HTTP/2 initial SETTINGS_HEADER_TABLE_SIZE).
fn Table::resize(self : Table, limit : Int32) -> Unit
Resize the table (a dynamic table size update, RFC 7541 §4.2), evicting to fit.
fn Table::add(self : Table, name : Bytes, value : Bytes) -> Unit
Insert (name, value) at the front, evicting oldest entries to make room. If the entry alone exceeds limit the table ends up empty (RFC 7541 §4.4).
fn Table::get(self : Table, index : Int32) -> @header.Header?
The field an HPACK index names, across the static and dynamic tables together (RFC 7541 §2.3.3): one through sixty-one are the static table, and everything above is this table, newest first. None when the index names nothing.
fn Table::limit(self : Table) -> Int32
The ceiling this table evicts to keep under, in octets.
fn Table::count(self : Table) -> Int32
The number of entries currently in the dynamic table.
fn Table::size(self : Table) -> Int32
The current total size of the dynamic table in octets (§4.1 accounting).
fn size_update(limit : Int32) -> Bytes
Encode a dynamic table size update (RFC 7541 §6.3): 001 prefix with the new maximum size as a 5-bit-prefix integer.
struct Decoder { table : Table mut limit : Int32 }
A stateful HPACK decoder: it owns a dynamic table that persists across the header blocks of a connection. limit is the peer-agreed hard cap (SETTINGS_HEADER_TABLE_SIZE) a size update may not exceed.
fn Decoder::new(limit~ : Int32 = limit) -> Decoder
A new decoder whose dynamic table is bounded by limit octets (also the hard cap enforced on dynamic table size updates).
fn Decoder::decode( self : Decoder, block : BytesView, limit? : Int32) -> Array[@header.Header] raise Refused
Decode one complete header block into its header list (RFC 7541 §6), mutating the dynamic table for incrementally indexed fields and size updates. limit bounds the list by RFC 7541 §4.1 size, each field's octets plus 32, which is what SETTINGS_MAX_HEADER_LIST_SIZE counts. Past it the block is still decoded to its end, so the dynamic table stays in step with the encoder, but no more fields are kept, and then Exceeded is raised. A block of a few octets that names large table entries over and over would otherwise decode to as much memory as the peer liked.
struct Encoder { table : Table mut huffman : Bool }
A stateful HPACK encoder: it owns a dynamic table mirroring the decoder's, and prefers indexed representations. huffman selects Huffman string literals when they are shorter.
fn Encoder::new(limit~ : Int32 = limit, huffman~ : Bool = true) -> Encoder
A new encoder bounded by limit octets; huffman (default true) enables the shorter-of-two string-literal heuristic.
fn Encoder::encode( self : Encoder, headers : ArrayView[@header.Header]) -> Bytes
Encode a header list into a header block (RFC 7541 §6), using indexed fields where possible and literal-with-incremental-indexing otherwise (mutating the dynamic table to mirror what the peer decoder will build). The output decodes back to the same header list via Decoder.
@qpack
RFC 9204 HTTP/3 header compression: both tables, the field lines, both instruction streams.
suberror Refused { Malformed(String) Index(got~ : Int32) Exceeded(limit~ : Int32, got~ : Int32) } derive(Eq, Debug)
Why a field section or an instruction was refused (RFC 9204 §2.2, §4). Each one is a connection error of its own kind in §6, because after it the two ends no longer agree about the dynamic table and nothing further can be read.
let static_table : Array[@header.Header] =
The static table as fields, in the index order Appendix A gives them. Unlike HPACK's, QPACK's static table is indexed from zero (RFC 9204 §3.1).
fn entry(index : Int32) -> @header.Header?
The entry at index, or None when the index names nothing.
fn find(name : Bytes, value : Bytes) -> Int32?
The index of the first entry whose name and value both match, for an indexed field line; None if no entry matches exactly.
fn find_name(name : Bytes) -> Int32?
The index of the first entry whose name matches (regardless of value), for a literal field line with a name reference; None if no entry has the name.
fn max_entries(capacity : Int32) -> Int32
The number of entries that fit in a table of capacity bytes at the 32-byte minimum entry size — MaxEntries in RFC 9204 §4.5.1.1.
fn prefix( req_insert_count : Int32, base : Int32, max_entries : Int32) -> Bytes
Encode a field-section prefix (RFC 9204 §4.5.1): the Required Insert Count transmitted modulo 2*max_entries, then the Base as a sign bit (0 when base >= req_insert_count) plus the delta.
fn insert_count( encoded : Int32, max_entries : Int32, total_inserts : Int32) -> Int32 raise Refused
Reconstruct the absolute Required Insert Count from its encoded form, max_entries, and the number of inserts the decoder has processed (RFC 9204 §4.5.1.1).
fn read_prefix( input : BytesView, max_entries : Int32, total_inserts : Int32) -> (Int32, Int32, Int32)? raise Refused
Decode a field-section prefix (RFC 9204 §4.5.1), given max_entries and the decoder's total inserts so far. Returns (req_insert_count, base, bytes-consumed), or None on a partial read.
struct Table { entries : Array[(Bytes, Bytes)] mut used : Int32 mut capacity : Int32 mut insert_count : Int32 mut dropped : Int32 }
A QPACK dynamic table: the live entries, the bytes they occupy, the capacity, the count of entries ever inserted (the next absolute index), and how many have been evicted.
fn Table::new(capacity : Int32) -> Table
A fresh, empty dynamic table with the given capacity in bytes.
fn cost(name : Bytes, value : Bytes) -> Int32
The size an entry occupies in the table: its name and value octets plus 32 (RFC 9204 §3.2.1).
fn Table::resize(self : Table, capacity : Int32) -> Unit
Set the table capacity, evicting whatever no longer fits (RFC 9204 §3.2.3).
fn Table::insert(self : Table, name : Bytes, value : Bytes) -> Bool
Insert an entry, evicting as needed. Returns false (adding nothing) when the entry cannot fit even in an empty table (RFC 9204 §3.2.2).
fn Table::get(self : Table, abs_index : Int32) -> (Bytes, Bytes)?
The entry at absolute index abs_index, or None if it has been evicted or never existed (RFC 9204 §3.2.4).
fn Table::relative(self : Table, rel : Int32) -> (Bytes, Bytes)?
The entry at encoder-stream relative index rel (0 = most recently inserted, RFC 9204 §3.2.5).
fn Table::apply(self : Table, inst : Change) -> Bool raise Refused
Apply an encoder-stream instruction to the table (RFC 9204 §4.3): set the capacity, or insert an entry whose name comes from the static table, a dynamic entry (by relative index), or a literal. Returns whether the resulting insert fit (always true for a capacity change). Raises if a referenced name index is out of range.
fn string( data : Bytes, prefix : Int32, pattern : Int32, huffman : Bool) -> Bytes
Encode a QPACK string literal: the H bit at position prefix (set when the data is Huffman-coded), pattern in the bits above it, the octet length as a prefix-prefix integer, then the (raw or Huffman) octets.
fn read_string( input : BytesView, prefix : Int32) -> (Bytes, Int32)? raise Refused
Decode a QPACK string literal whose length uses a prefix-prefix integer and whose H bit sits at position prefix. Returns (octets, bytes-consumed), or None when the string has not fully arrived.
fn encode(headers : Array[@header.Header], huffman~ : Bool = true) -> Bytes
Encode a header list as a QPACK field section over the static table only: a zero-insert-count / zero-base prefix, then one field line per header — an indexed static entry for an exact match, a static name reference plus a literal value when the name is known, or a fully literal name and value otherwise. huffman chooses whether literal strings are Huffman-coded.
fn decode(input : Bytes) -> Array[@header.Header] raise Refused
Decode a QPACK field section (encoded over the static table) back into its header list. The prefix's insert count and base are read and required to be zero (this decoder resolves static references only); a field line that references the dynamic table is reported as unsupported.
fn decode_with( input : Bytes, table : Table, max_entries : Int32) -> Array[@header.Header] raise Refused
Decode a QPACK field section that may reference the dynamic table, given max_entries (from the negotiated maximum table capacity). Reads the section prefix, then every field line — static or dynamic, indexed or literal, pre- or post-base — into the header list. Raises if a reference is out of range or the section is malformed.
fn encode_with( headers : Array[@header.Header], table : Table, max_entries : Int32, huffman~ : Bool = true) -> (Bytes, Bytes)
Encode headers with dynamic indexing over table (the encoder's view, mutated by the inserts). Returns the encoder-stream instruction block and the field section. max_entries comes from the negotiated maximum table capacity.
enum Change { /// Set Dynamic Table Capacity (§4.3.1): `001` + capacity (5-bit prefix). SetCapacity(Int32) /// Insert With Name Reference (§4.3.2): `1` + T (static/dynamic) + name index (6-bit /// prefix), then the value string. Insert(is_static~ : Bool, index~ : Int32, value~ : Bytes) /// Insert With Literal Name (§4.3.3): `01` + H + name length (5-bit prefix) + name, /// then the value string. InsertLiteral(name~ : Bytes, value~ : Bytes) /// Duplicate (§4.3.4): `000` + index (5-bit prefix). Duplicate(index~ : Int32) } derive(Eq, Debug)
An encoder-stream instruction (RFC 9204 §4.3).
enum Ack { /// Section Acknowledgement (§4.4.1): `1` + stream id (7-bit prefix). Section(stream_id~ : Int32) /// Stream Cancellation (§4.4.2): `01` + stream id (6-bit prefix). Cancel(stream_id~ : Int32) /// Insert Count Increment (§4.4.3): `00` + increment (6-bit prefix). Increment(increment~ : Int32) } derive(Eq, Debug)
A decoder-stream instruction (RFC 9204 §4.4).
fn Change::encode(inst : Change, huffman~ : Bool = true) -> Bytes
Encode an encoder-stream instruction (RFC 9204 §4.3). huffman chooses whether the literal strings of the insert instructions are Huffman-coded.
fn Change::decode(input : BytesView) -> (Change, Int32)? raise Refused
Decode one encoder-stream instruction from the front of input (RFC 9204 §4.3), dispatching on the pattern in the first byte's high bits. Returns (instruction, bytes-consumed), or None when the instruction has not fully arrived.
fn Ack::encode(inst : Ack) -> Bytes
Encode a decoder-stream instruction (RFC 9204 §4.4).
fn Ack::decode(input : BytesView) -> (Ack, Int32)? raise Refused
Decode one decoder-stream instruction from the front of input (RFC 9204 §4.4). Returns (instruction, bytes-consumed), or None on a partial read.
@http1
RFC 9112 messages: request and status lines, field lines, body framing, the chunked coding; and Conn, a whole connection on h11's state machine.
suberror Refused { Partial Malformed(String) Exceeded(limit~ : Int32, got~ : Int32) Smuggled(String) Order(String) } derive(Eq, Debug)
Why a message was refused. Partial is not a fault: it is the reader saying the bytes have not all arrived, which is the answer a caller feeds more and asks again. Order is this side's mistake, an event sent where the exchange does not allow it. Everything else is a peer's message that will not become valid by waiting.
fn Refused::status(self : Refused) -> Int32
The status a refusal deserves, for a server that answers rather than hangs up. A message too large is 431 when it is the head (RFC 6585 §5) and 413 when it is the body; this is about the head, which is what this package bounds. A framing fault is 400.
enum Body { Empty Sized(Int32) Chunked // Until the connection closes, which only a response may use (§6.3). Closed } derive(Eq, Debug)
How a message's body is delimited (RFC 9112 §6). The three ways are not interchangeable and picking the wrong one is how a request gets smuggled past a proxy, so this is read from the headers once and carried rather than re-derived.
struct Limits { head : Int32 fields : Int32 } derive(Eq, Debug)
The limits a reader holds a peer to. The preset is [limits]. Build another with [Limits::new], or update one in place with { ..limits, head: 8192 }.
let limits : Limits =
Sixteen kibibytes of head and a hundred fields. 16 KiB is what h11 caps an incomplete event at and what uvicorn exposes as --h11-max-incomplete-event-size; nginx's large_client_header_buffers and Go's MaxHeaderBytes land in the same place. A hundred fields is Go's http2.Server default and more than any real request carries.
fn Limits::new( head~ : Int32 = limits.head, fields~ : Int32 = limits.fields) -> Limits
Limits by name, every knob with the preset's value.
struct Request { verb : String target : String version : String fields : Array[@header.Header] body : Body // `Expect: 100-continue` — the client is waiting to be told to send the body. expects : Bool keep_alive : Bool } derive(Eq, Debug)
A request's head: the request line and the fields under it. Field names are lowercased, because HTTP/1.1 says they are case-insensitive and every consumer would otherwise lowercase them again. Values are octets, not text: a field value may carry anything, and gRPC's -bin metadata does.
struct Response { version : String status : Int32 reason : String fields : Array[@header.Header] body : Body keep_alive : Bool } derive(Eq, Debug)
A response's head: the status line and the fields under it.
fn Request::field(self : Request, name : String) -> Bytes?
The value of the first field named name, or None.
fn Response::field(self : Response, name : String) -> Bytes?
The same, for a response.
enum Role { Server Client } derive(Eq, Debug)
Which end of a connection a [Conn] is.
enum State { Idle SendResponse SendBody Done MustClose Closed MightSwitch Switched Error } derive(Eq, Debug)
Where one side of a connection is in its half of an exchange. These are h11's states (https://h11.readthedocs.io/en/latest/api.html), one per side, read with [Conn::ours] and [Conn::theirs]. A server that has read a request is SendResponse until it answers. A side whose exchange said Connection: close ends in MustClose rather than Done. A client that asked to upgrade waits in MightSwitch until the server says yes, which is Switched for both, or no, which puts it back to Done.
enum Event { Request(Request) Response(Response) // A 1xx: `100 Continue`, or the `101` that switches protocols. Informational(Response) Data(Bytes) // The end of a message, with its trailer fields. End(Array[@header.Header]) NeedData Paused // Read, the peer closed its side; sent, this side is closing its own. Closed } derive(Eq, Debug)
What a connection reads from its peer, or is given to write. A message is always a head, any number of Data, then End, even when the body is empty, so a consumer handles one sequence. NeedData and Paused are only ever read: the first asks for more octets, the second says the next message waits for [Conn::start_next] or, after a switch, belongs to another protocol.
fn Conn::new(role : Role, limits~ : Limits = limits) -> Conn
A connection at its start, both sides Idle. limits bounds what the peer may send before a message is complete: its head, a chunk-size line, a trailer section. Bodies are handed over as they arrive and are not bounded here.
fn Conn::ours(self : Conn) -> State
This side's state.
fn Conn::theirs(self : Conn) -> State
The peer's state.
fn Conn::receive(self : Conn, data : BytesView) -> Unit raise Refused
Octets read from the peer. Empty means the peer closed its side. Raises [Refused::Order] for octets after that, which no transport delivers.
fn Conn::next(self : Conn) -> Event raise Refused
The next event from the peer. Bodies come as Data in the pieces they arrived in, whether sized, chunked or running to the close. NeedData asks for [Conn::receive]; while it is returned, the octets held past limits.head raise [Refused::Exceeded]. A head that does not parse raises what [request] or [response] would, an event out of order raises Malformed, and either way the peer's state becomes Error and the connection will not be reused. A server may still answer it: [Refused::status] gives the status.
fn Conn::send(self : Conn, event : Event) -> Bytes raise Refused
The octets that write event to the peer. A response's framing is decided here rather than trusted. A Content-Length field is used as given. Without one the body is chunked to an HTTP/1.1 peer and runs to the close otherwise, with the fields rewritten to say so. A response to HEAD keeps the fields a GET would get and drops the body. A Connection: close is added whenever the connection will not be reused, and Connection: keep-alive for an HTTP/1.0 peer when it will. A request is written as given, framed by its own fields. Raises [Refused::Order] for an event this side may not send now: data before a head, more body than its Content-Length, a body that ends short of it. This side's state then becomes Error.
fn Conn::start_next(self : Conn) -> Unit raise Refused
Begin the next exchange on a kept-alive connection. Both sides must be Done; a side in MustClose means the connection is to be closed instead. A pipelined request already received is read by the next [Conn::next].
fn Conn::trailing(self : Conn) -> Bytes
The octets received but not read as HTTP. After a switch these belong to the new protocol: a WebSocket frame that came in the same segment as the upgrade request, for instance.
fn request( data : BytesView, limits~ : Limits = limits) -> (Request, Int32) raise Refused
The request at the front of data, and how many octets it took. Raises [Refused::Partial] while the head has not all arrived, which is the answer a caller reads more on. This reads the head only: the body is framed by [Request::body] and is the caller's to collect, because a body may be larger than anything worth holding in one buffer.
fn response( data : BytesView, verb~ : String = "GET", limits~ : Limits = limits) -> (Response, Int32) raise Refused
The response at the front of data, and how many octets it took. verb is the one the request used, because it decides the framing: a response to HEAD has no body however its fields read (RFC 9112 §6.3).
fn Request::encode(self : Request) -> Bytes
A request head as octets. The fields are written as given; this does not add a Host, a Content-Length or anything else, because a caller that wanted those would have put them in and one that did not would be surprised to find them.
fn Response::encode(self : Response) -> Bytes
A response head as octets.
fn Request::of( verb : String, target : String, fields~ : Array[@header.Header] = [], version~ : String = "HTTP/1.1", body~ : Body = Empty, host~ : String = "", keep_alive~ : Bool = true) -> Request
A request by name, with the framing worked out from what it is given. body decides what is written: a sized body gets a Content-Length, a chunked one a Transfer-Encoding, and neither gets neither. That is the one thing a hand-built request gets wrong, so it is not left to the caller.
fn Response::of( status : Int32, fields~ : Array[@header.Header] = [], reason~ : String = "", version~ : String = "HTTP/1.1", body~ : Body = Empty, keep_alive~ : Bool = true) -> Response
A response by name, with the framing worked out the same way.
fn phrase(status : Int32) -> String
The reason phrase a status is usually written with (RFC 9110 §15). A client must not act on it (§4), so this is for the human reading a capture.
let continue_ : Bytes = b"HTTP/1.1 100 Continue\r\n\r\n"
The interim 100 Continue a server sends when a request said it was waiting (RFC 9110 §10.1.1).
fn chunk(data : BytesView) -> Bytes
One chunk: its size in hexadecimal, the octets, and a break (RFC 9112 §7.1).
fn last_chunk(trailers~ : Array[@header.Header] = []) -> Bytes
The zero-size chunk that ends a body, with optional trailer fields after it.
enum Chunk { // Octets of the body, and how many octets of `data` they took. Piece(Bytes, Int32) // The body ended; its trailer fields, and how many octets that took. Done(Array[@header.Header], Int32) } derive(Eq)
What a chunked body reader found at the front of data.
fn chunked(data : BytesView) -> Chunk raise Refused
The chunk at the front of data. Raises [Refused::Partial] while it has not all arrived. A size line longer than sixteen hexadecimal digits is refused rather than accumulated: a size that needs more than sixty-four bits is not a size.
@http2
RFC 9113 framing: the nine-octet header, all ten frame types, the connection preface; and Conn, a whole connection after python-hyper h2.
let preface : Bytes = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
The HTTP/2 client connection preface (RFC 9113 §3.5): the fixed, case-sensitive 24-octet sequence PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n a client sends before its first frame, which must be a SETTINGS frame. Its bytes deliberately form a malformed HTTP/1.1 request so an HTTP/1.1-only server rejects it cleanly.
fn has_preface(data : Bytes) -> Bool
Whether data begins with the exact 24-octet HTTP/2 connection preface.
let frame_data : Int32 = 0x0
HTTP/2 frame type codes (RFC 9113 §6).
let frame_headers : Int32 = 0x1
HEADERS: opens a stream and carries its HPACK header block.
let frame_priority : Int32 = 0x2
PRIORITY: the deprecated stream-dependency hint; accepted and ignored.
let frame_rst_stream : Int32 = 0x3
RST_STREAM: abort one stream without touching the connection.
let frame_settings : Int32 = 0x4
SETTINGS: the connection parameters each peer announces, and their ack.
let frame_push_promise : Int32 = 0x5
PUSH_PROMISE: server push, which gRPC never uses.
let frame_ping : Int32 = 0x6
PING: the round-trip probe keepalive is built on.
let frame_goaway : Int32 = 0x7
GOAWAY: stop opening streams, and here is the last id I accepted.
let frame_window_update : Int32 = 0x8
WINDOW_UPDATE: hand the peer more flow-control credit.
let frame_continuation : Int32 = 0x9
CONTINUATION: the rest of a header block too large for one frame.
let flag_end_stream : Int32 = 0x1
HTTP/2 frame flags (RFC 9113 §6). Flags are type-specific; the same bit carries different meaning per frame type, hence the shared numeric values.
let flag_ack : Int32 = 0x1
ACK on SETTINGS and PING shares bit 0x1 with END_STREAM.
let flag_end_headers : Int32 = 0x4
END_HEADERS: no CONTINUATION follows this header block.
let flag_padded : Int32 = 0x8
PADDED: a length byte and that many padding bytes wrap the payload.
let flag_priority : Int32 = 0x20
PRIORITY: this HEADERS frame carries a stream-dependency block.
let settings_header_table_size : Int32 = 0x1
SETTINGS parameter identifiers (RFC 9113 §6.5.2).
let settings_enable_push : Int32 = 0x2
SETTINGS_ENABLE_PUSH: whether the peer may send PUSH_PROMISE.
let settings_max_concurrent_streams : Int32 = 0x3
SETTINGS_MAX_CONCURRENT_STREAMS: how many streams may be open at once.
let settings_initial_window_size : Int32 = 0x4
SETTINGS_INITIAL_WINDOW_SIZE: the flow-control credit a new stream starts with.
let settings_max_frame_size : Int32 = 0x5
SETTINGS_MAX_FRAME_SIZE: the largest payload the peer will accept in one frame.
let settings_max_header_list_size : Int32 = 0x6
SETTINGS_MAX_HEADER_LIST_SIZE: the advisory ceiling on a decoded header list.
let settings_enable_connect_protocol : Int32 = 0x8
SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC 8441 §3): the extended CONNECT that carries WebSockets over HTTP/2.
let error_no_error : Int32 = 0x0
HTTP/2 error codes (RFC 9113 §7), carried by RST_STREAM and GOAWAY.
let error_protocol_error : Int32 = 0x1
PROTOCOL_ERROR: the peer broke the protocol in a way not covered below.
let error_internal_error : Int32 = 0x2
INTERNAL_ERROR: the endpoint failed for reasons of its own.
let error_flow_control_error : Int32 = 0x3
FLOW_CONTROL_ERROR: the peer sent more than its credit allowed.
let error_settings_timeout : Int32 = 0x4
SETTINGS_TIMEOUT: a SETTINGS frame went unacknowledged too long.
let error_stream_closed : Int32 = 0x5
STREAM_CLOSED: a frame arrived for a stream that was already finished.
let error_frame_size_error : Int32 = 0x6
FRAME_SIZE_ERROR: the frame's length is invalid for its type.
let error_refused_stream : Int32 = 0x7
REFUSED_STREAM: the stream was declined before any processing, so it is safe to retry.
let error_cancel : Int32 = 0x8
CANCEL: the stream is no longer wanted.
let error_compression_error : Int32 = 0x9
COMPRESSION_ERROR: the HPACK context is corrupt, which kills the connection.
let error_connect_error : Int32 = 0xA
CONNECT_ERROR: the TCP connection behind a CONNECT stream failed.
let error_enhance_your_calm : Int32 = 0xB
ENHANCE_YOUR_CALM: the peer is generating too much load.
let error_inadequate_security : Int32 = 0xC
INADEQUATE_SECURITY: the transport does not meet the minimum this endpoint requires.
let error_http_1_1_required : Int32 = 0xD
HTTP_1_1_REQUIRED: the request cannot be served over HTTP/2.
struct Priority { exclusive : Bool stream_dependency : Int32 weight : Int32 } derive(Eq)
The stream-priority block shared by PRIORITY frames and the optional priority section of HEADERS (RFC 9113 §5.3.2). weight is the raw wire byte (0..255); the effective priority weight is weight + 1 (§6.3).
suberror Refused { Incomplete FrameSizeError(String) ProtocolError(String) Connection(code~ : Int32, String) Order(String) } derive(Eq)
Why a frame or a connection was refused. Incomplete is the normal "need more bytes" signal a streaming reader catches to wait for the rest, and FrameSizeError and ProtocolError are frames that do not decode. A [Conn] raises only the last two kinds: Connection when the peer broke the connection, with the GOAWAY that says so already queued, and Order when this side sent something the protocol does not allow now.
enum Frame { Data(stream_id~ : Int32, data~ : Bytes, end_stream~ : Bool, padding~ : Int32) Headers( stream_id~ : Int32, fragment~ : Bytes, end_stream~ : Bool, end_headers~ : Bool, priority~ : Priority?, padding~ : Int32 ) Priority(stream_id~ : Int32, priority~ : Priority) RstStream(stream_id~ : Int32, error_code~ : Int32) Settings(params~ : Array[(Int32, Int32)], ack~ : Bool) PushPromise( stream_id~ : Int32, promised_id~ : Int32, fragment~ : Bytes, end_headers~ : Bool, padding~ : Int32 ) Ping(payload~ : Bytes, ack~ : Bool) GoAway(last_stream_id~ : Int32, error_code~ : Int32, debug~ : Bytes) WindowUpdate(stream_id~ : Int32, increment~ : Int32) Continuation(stream_id~ : Int32, fragment~ : Bytes, end_headers~ : Bool) Unknown(ftype~ : Int32, flags~ : Int32, stream_id~ : Int32, payload~ : Bytes) } derive(Eq)
A decoded HTTP/2 frame (RFC 9113 §6). Each variant carries the semantic payload with padding already stripped; padding is the number of padding bytes to (re)emit. Unknown preserves extension/unrecognised frames verbatim so a reader can forward or ignore them (RFC 9113 §4.1).
struct FrameHeader { length : Int32 ftype : Int32 flags : Int32 stream_id : Int32 } derive(Eq)
The fixed 9-octet frame header (RFC 9113 §4.1): a 24-bit payload length, an 8-bit type, 8-bit flags, a reserved bit, and a 31-bit stream identifier.
fn FrameHeader::encode(self : FrameHeader) -> Bytes
Encode a FrameHeader to its 9 wire octets.
fn FrameHeader::decode( data : Bytes, offset~ : Int32 = 0) -> FrameHeader raise Refused
Decode the 9-octet frame header at offset. Raises Incomplete when fewer than 9 octets are available. The reserved bit is masked off the stream id.
fn Frame::code(self : Frame) -> Int32
The numeric frame-type code of this frame (RFC 9113 §6).
fn Frame::encode(self : Frame) -> Bytes
Encode this frame to its complete wire representation (9-octet header + payload), the exact inverse of decode_frame.
fn decode( data : Bytes, offset~ : Int32 = 0) -> (Frame, Int32) raise Refused
Decode exactly one frame at offset, returning (frame, bytes_consumed) where bytes_consumed is 9 + payload_length. Raises Incomplete when the buffer does not yet hold the whole frame, or a protocol error when the payload is malformed for its type. The inverse of Frame::encode.
enum Role { Server Client } derive(Eq, Debug)
Which end of a connection a [Conn] is.
enum Event { Request(stream~ : Int32, fields~ : Array[@header.Header]) Response(stream~ : Int32, fields~ : Array[@header.Header]) // A 1xx response, of which a stream may have several before its final one. Informational(stream~ : Int32, fields~ : Array[@header.Header]) Trailers(stream~ : Int32, fields~ : Array[@header.Header]) Data(stream~ : Int32, data~ : Bytes) End(stream~ : Int32) Reset(stream~ : Int32, code~ : Int32, remote~ : Bool) // The server promised `promised`, a response to the request in `fields`, on // the client's stream `stream`. Pushed(stream~ : Int32, promised~ : Int32, fields~ : Array[@header.Header]) // Stream 0 is the connection. WindowUpdated(stream~ : Int32, delta~ : Int32) // The peer's settings, now in force. Settings(Settings) // This side's settings, now that the peer has acknowledged them. SettingsAcked(Settings) Ping(Bytes) PingAcked(Bytes) GoAway(code~ : Int32, last~ : Int32, debug~ : Bytes) NeedData // The peer closed the transport. Closed } derive(Eq, Debug)
What a connection reads from its peer. As in http1, a message is a head, any number of Data, then End; trailers come as Trailers before the End. Reset with remote=false is a stream this side refused, with the RST_STREAM already queued. Settings and Ping have already been acknowledged when they are read.
fn Conn::new(role : Role, settings~ : Settings = settings) -> Conn
A connection at its start, with its opening frames queued: the client's preface and SETTINGS, or the server's SETTINGS (§3.4). settings take effect when the peer acknowledges them; until then both ends go by [initial]. Settings outside what §6.5.2 allows abort, since no peer could accept them.
fn Conn::receive(self : Conn, data : BytesView) -> Unit raise Refused
Octets read from the peer. Empty means the peer closed the transport.
fn Conn::next(self : Conn) -> Event raise Refused
The next event from the peer. A connection error raises Connection with the GOAWAY already queued in [Conn::take], after which the connection is finished. A stream error does not raise: the RST_STREAM is queued and the stream ends with Reset.
fn Conn::take(self : Conn) -> Bytes
The octets queued for the peer since the last call.
fn Conn::settings(self : Conn) -> Settings
This side's settings in force.
fn Conn::peer_settings(self : Conn) -> Settings
The peer's settings in force.
fn Conn::next_stream(self : Conn) -> Int32
The id the next stream this side opens, or the next push it promises, will have.
fn Conn::window(self : Conn, stream : Int32) -> Int32
How much this side may send on stream now, the smaller of its own window and the connection's; stream 0 is the connection alone.
fn Conn::headers( self : Conn, stream : Int32, fields : Array[@header.Header], end~ : Bool = false) -> Unit raise Refused
Send a head on stream: a client's request on a stream it opens with this, a server's informational or final response, or either side's trailers. Which one it is follows from the stream: a new id from a client is a request, the first head a server sends is its response (a 1xx leaves room for another), and a head after the final one is the trailers, which must end the stream. The fields are checked against RFC 9113 §8.2 and §8.3 as a peer would check them, and a block longer than the peer's frame size is carried on in CONTINUATION frames. Raises [Refused::Order] for a head the protocol does not allow here.
fn Conn::data( self : Conn, stream : Int32, data : BytesView, end~ : Bool = false) -> Unit raise Refused
Send data on stream, in frames no longer than the peer allows. It must fit in [Conn::window]; a caller with more waits for WindowUpdated. Empty data with end sends the END_STREAM alone.
fn Conn::end(self : Conn, stream : Int32) -> Unit raise Refused
End stream from this side with an empty DATA frame.
fn Conn::reset( self : Conn, stream : Int32, code~ : Int32 = error_no_error) -> Unit raise Refused
Reset stream (§6.4). NO_ERROR is h2's default too: a reset is as often a caller that no longer wants the stream as a fault.
fn Conn::push( self : Conn, stream : Int32, fields : Array[@header.Header]) -> Int32 raise Refused
Promise the response to the request in fields on the client's stream, and the id of the new stream the response goes on (§8.4). The response is then sent with [Conn::headers] and [Conn::data] on the returned id.
fn Conn::ping(self : Conn, payload : Bytes) -> Unit raise Refused
Send a PING with eight octets of payload; the answer comes as PingAcked.
fn Conn::goaway( self : Conn, code~ : Int32 = error_no_error, debug~ : Bytes = b"") -> Unit raise Refused
Tell the peer this side will process no stream past the last one it opened (§6.8). Streams already open carry on; new ones from the peer are ignored.
fn Conn::update(self : Conn, settings : Settings) -> Unit raise Refused
Move this side to settings. They take effect when the peer acknowledges them, reported as SettingsAcked.
fn Conn::acknowledge( self : Conn, stream : Int32, size : Int32) -> Unit raise Refused
Hand back size octets of stream's data, now processed, so the peer may send more (§6.9). WINDOW_UPDATE goes out when half a window has come back, as h2 does; a caller that never acknowledges stalls the peer.
fn Conn::widen( self : Conn, stream : Int32, by : Int32) -> Unit raise Refused
Widen the receive window of stream, or of the connection for stream 0, by by octets at once, beyond what acknowledgement would.
struct Settings { header_table_size : Int32 enable_push : Bool max_concurrent_streams : Int32? initial_window_size : Int32 max_frame_size : Int32 max_header_list_size : Int32? enable_connect_protocol : Bool } derive(Eq, Debug)
An endpoint's settings (RFC 9113 §6.5.2, RFC 8441 §3). None is the RFC's "unlimited". The preset is [settings]; [initial] is what both ends assume before any SETTINGS frame, and so what the peer's settings start from. Build others with [Settings::new] or { ..settings, max_frame_size: n }.
let initial : Settings =
The value RFC 9113 §6.5.2 gives every setting before an endpoint has said otherwise.
let settings : Settings =
What a connection advertises unless told otherwise: the RFC's values, with a hundred concurrent streams and 64 KiB of header list. Those two are python-hyper h2's own defaults, and a hundred streams is also where nghttp2 and gRPC settle. Left unlimited, every stream a peer opens and never finishes holds memory, and a few octets of HPACK can name one large table entry thousands of times.
fn Settings::new( header_table_size~ : Int32 = settings.header_table_size, enable_push~ : Bool = settings.enable_push, max_concurrent_streams~ : Int32? = settings.max_concurrent_streams, initial_window_size~ : Int32 = settings.initial_window_size, max_frame_size~ : Int32 = settings.max_frame_size, max_header_list_size~ : Int32? = settings.max_header_list_size, enable_connect_protocol~ : Bool = settings.enable_connect_protocol) -> Settings
Settings by name, every one with the preset's value.
@http3
RFC 9114 frames, their placement rules, settings, stream types and connection.
suberror Refused { Malformed(String) } derive(Eq, Debug)
Why a frame, a stream or a connection was refused. RFC 9114 §8 gives each case its own connection error; they are one type here because the answer to every one of them is the same — close the connection — and the message says which it was.
enum Frame { Data(Bytes) Headers(Bytes) CancelPush(UInt64) Settings(Array[(UInt64, UInt64)]) PushPromise(UInt64, Bytes) GoAway(UInt64) MaxPushId(UInt64) Reserved(UInt64, Bytes) ReservedHttp2(UInt64) } derive(Eq, Debug)
A decoded HTTP/3 frame. Data/Headers/PushPromise carry their opaque payloads; Settings carries the decoded (identifier, value) pairs. Reserved is any unknown or grease frame type (RFC 9114 §7.2.8 / §9), which a receiver ignores; ReservedHttp2 is one of the frame types reused from HTTP/2 (0x02, 0x06, 0x08, 0x09), which RFC 9114 §7.2 requires be treated as a connection error.
fn Frame::encode(frame : Frame) -> Bytes
Encode one HTTP/3 frame: its type, its payload length, then the payload.
fn settings(pairs : Array[(UInt64, UInt64)]) -> Bytes
Encode a SETTINGS payload: each pair as its identifier varint then its value varint (RFC 9114 §7.2.4).
fn decode(input : BytesView) -> (Frame, Int32)? raise Refused
Decode one HTTP/3 frame from the front of input. Returns Some((frame, consumed)) on a complete frame, None when more bytes are needed (a partial frame — the type, the length, or the payload has not fully arrived), and raises on a malformed frame (a truncated SETTINGS pair or a required varint field that is absent).
fn read_settings(payload : Bytes) -> Array[(UInt64, UInt64)] raise Refused
Decode a SETTINGS payload into its (identifier, value) pairs. A trailing partial pair (an identifier with no value, or a truncated varint) is a frame error.
fn decode_all(input : Bytes) -> (Array[Frame], Int32) raise Refused
Decode every complete frame at the front of input, returning the frames and the number of octets consumed (the tail, a partial frame, is left for the next read). A stream reader feeds accumulated bytes and keeps the unconsumed remainder.
enum On { Control Request Push } derive(Eq, Debug)
The stream a frame was received on, for the purpose of frame-placement rules (RFC 9114 §6.2).
fn allowed(frame : Frame, ctx : On) -> Bool
Whether frame is permitted on a stream of kind ctx (RFC 9114 §7.2).
fn check(frame : Frame, ctx : On) -> Unit raise Refused
Raise Refused (H3_FRAME_UNEXPECTED) when frame is not permitted on ctx.
let qpack_capacity : UInt64 = 0x01
SETTINGS_QPACK_MAX_TABLE_CAPACITY (RFC 9204 §5).
let max_field_section : UInt64 = 0x06
SETTINGS_MAX_FIELD_SECTION_SIZE (RFC 9114 §7.2.4.1).
let blocked_streams : UInt64 = 0x07
SETTINGS_QPACK_BLOCKED_STREAMS (RFC 9204 §5).
struct Settings { qpack_max_table_capacity : UInt64 max_field_section_size : UInt64 qpack_blocked_streams : UInt64 } derive(Eq, Debug)
An endpoint's HTTP/3 settings.
fn Settings::new( qpack_max_table_capacity : UInt64, max_field_section_size : UInt64, qpack_blocked_streams : UInt64) -> Settings
Settings with the given QPACK table capacity, maximum field section size, and blocked-stream limit.
fn Settings::pairs(self : Settings) -> Array[(UInt64, UInt64)]
The (identifier, value) pairs to carry in a SETTINGS frame.
fn Settings::of_pairs(pairs : Array[(UInt64, UInt64)]) -> Settings
Read settings from received (identifier, value) pairs, taking the value for each known identifier (0 when absent) and ignoring unknown ones (RFC 9114 §7.2.4.1 grease tolerance).
fn read_control(input : BytesView) -> Settings? raise Refused
Decode an endpoint's control stream (RFC 9114 §6.2.1): its type prefix must be the control stream, and its first frame a SETTINGS frame, whose settings are returned. None if the stream is not a control stream or does not open with SETTINGS. Raises on a malformed frame.
enum Stream { ControlKind PushKind(UInt64) EncoderKind DecoderKind ReservedKind(UInt64) } derive(Eq, Debug)
An HTTP/3 unidirectional stream's type (RFC 9114 §6.2, RFC 9204 §4.2).
fn Stream::encode(stream : Stream) -> Bytes
Encode a unidirectional stream's opening bytes: the type varint, plus the push id for a push stream (RFC 9114 §6.2.2).
fn Stream::decode(input : BytesView) -> (Stream, Int32)?
Decode a unidirectional stream's opening bytes: the type varint (and a push id for a push stream). Returns the stream kind and bytes consumed, or None on a partial read.
fn control(settings : Array[(UInt64, UInt64)]) -> Bytes
Encode a control stream (RFC 9114 §6.2.1): the control-stream type, then a SETTINGS frame with settings, which a control stream must send first.
fn message(headers : Array[@header.Header], body : Bytes) -> Bytes
Encode a message body as an HTTP/3 request/response stream: a HEADERS frame with the QPACK-encoded headers (static table only), then a DATA frame when body is non-empty.
fn read_message( bytes : Bytes) -> (Array[@header.Header], Bytes) raise Refused
Decode an HTTP/3 request/response stream back into its header list and body: the first HEADERS frame gives the headers, the DATA frames concatenate into the body, and a second HEADERS frame (trailers) and unknown frames are ignored (RFC 9114 §4.1). Raises if there is no HEADERS frame or a frame is malformed.
fn request( meth : String, scheme : String, authority : String, path : String, headers : Array[@header.Header], body : Bytes) -> Bytes
Encode an HTTP/3 request: the :method, :scheme, :authority, and :path pseudo- headers (RFC 9114 §4.3.1), then headers, then the body.
fn response( status : String, headers : Array[@header.Header], body : Bytes) -> Bytes
Encode an HTTP/3 response: the :status pseudo-header (RFC 9114 §4.3.2), then headers, then the body.
enum Role { Client Server } derive(Eq, Debug)
Which side of the connection an endpoint is (RFC 9114 §3.1): a server must not receive a push stream, so the role decides whether one is a protocol violation.
enum Event { ControlEstablished(Settings) QpackEncoder QpackDecoder Push(UInt64) Reserved(UInt64) } derive(Eq, Debug)
The result of accepting a peer's unidirectional stream: the control stream carries the peer's SETTINGS, the QPACK streams carry table instructions, a push stream carries its id, and an unrecognized type is reserved/greased.
struct Conn { role : Role local_settings : Settings mut peer_settings : Settings? mut control_seen : Bool }
One endpoint of an HTTP/3 connection: its role, the settings it advertises, and — once the peer opens its control stream — the peer's settings.
fn Conn::new(role : Role, local_settings : Settings) -> Conn
A connection that will advertise local_settings.
fn Conn::open_control_stream(self : Conn) -> Bytes
The bytes to write when opening our control stream: the control-stream type then our SETTINGS frame, which a control stream must send first (RFC 9114 §6.2.1).
fn Conn::accept_uni_stream(self : Conn, bytes : Bytes) -> Event raise
Accept a peer's unidirectional stream from its opening bytes: classify it and, for the control stream, decode and keep the peer's SETTINGS. A second control stream, a control stream not opening with SETTINGS, and a server receiving a push stream are all connection errors (RFC 9114 §6.2).
fn Conn::settings_established(self : Conn) -> Bool
Whether the peer's control-stream SETTINGS have been received (connection setup complete).
fn Conn::peer_settings(self : Conn) -> Settings?
The peer's advertised settings, once its control stream has been accepted.
struct Request { meth : String scheme : String authority : String path : String headers : Array[@header.Header] body : Bytes }
A decoded HTTP/3 request: the pseudo-headers pulled out, the remaining header fields, and the body.
fn read_request( bytes : Bytes, strict~ : Bool = false) -> Request? raise Refused
Decode an HTTP/3 request stream into a request, separating the pseudo-headers from the regular ones (RFC 9114 §4.3.1). None when the required :method and :path are absent. Raises on a malformed stream. strict also applies §7.2's placement rules, so a frame that belongs on the control stream — a SETTINGS, say — is refused rather than read past. It is off by default because a reader that only wants the request need not care what else arrived.
struct Response { status : String headers : Array[@header.Header] body : Bytes }
An HTTP/3 response: the status, the header fields, and the body.
fn Response::encode(self : Response) -> Bytes
Encode the response as an HTTP/3 response stream (a HEADERS frame with :status, then DATA).
fn serve( request_bytes : Bytes, handler : (Request) -> Response) -> Bytes raise Refused
Serve one HTTP/3 request stream: decode the request, apply handler, and encode its response stream. A request missing its pseudo-headers is answered with a 400. Raises on a malformed request stream.