// The HTTP/3 frame layer (RFC 9114 §7.2): every frame on an HTTP/3 stream is a
// variable-length `Type`, a variable-length `Length`, and that many payload octets.
// This is the mapping that turns a QUIC stream's bytes into the DATA / HEADERS /
// SETTINGS / GOAWAY / … frames HTTP/3 runs on, built directly on the QUIC varint codec.
// DATA and HEADERS carry opaque payloads (a body, or a QPACK-encoded field section —
// QPACK is a separate layer); SETTINGS is decoded into its identifier/value pairs.
///|
/// A malformed HTTP/3 frame (a protocol error, distinct from an incomplete read).
pub suberror Http3FrameError {
Http3FrameError(String)
}
///|
/// HTTP/3 frame type codes (RFC 9114 §11.2.1).
let h3_frame_data : UInt64 = 0x00
///|
let h3_frame_headers : UInt64 = 0x01
///|
let h3_frame_cancel_push : UInt64 = 0x03
///|
let h3_frame_settings : UInt64 = 0x04
///|
let h3_frame_push_promise : UInt64 = 0x05
///|
let h3_frame_goaway : UInt64 = 0x07
///|
let h3_frame_max_push_id : UInt64 = 0x0d
///|
/// 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.
pub(all) enum Http3Frame {
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)
///|
/// Whether `t` is an HTTP/2 frame type reused in HTTP/3's registry with no HTTP/3
/// meaning (PRIORITY, PING, WINDOW_UPDATE, CONTINUATION) — a connection error on
/// receipt (RFC 9114 §11.2.1).
fn h3_is_reserved_http2(t : UInt64) -> Bool {
t == 0x02 || t == 0x06 || t == 0x08 || t == 0x09
}
///|
/// Encode one HTTP/3 frame: its type, its payload length, then the payload.
pub fn http3_frame_encode(frame : Http3Frame) -> Bytes {
let (frame_type, payload) = match frame {
Data(body) => (h3_frame_data, body)
Headers(block) => (h3_frame_headers, block)
CancelPush(id) => (h3_frame_cancel_push, quic_varint_encode(id))
Settings(pairs) => (h3_frame_settings, http3_settings_encode(pairs))
PushPromise(id, block) => (h3_frame_push_promise, http3_prefixed(id, block))
GoAway(id) => (h3_frame_goaway, quic_varint_encode(id))
MaxPushId(id) => (h3_frame_max_push_id, quic_varint_encode(id))
Reserved(t, body) => (t, body)
ReservedHttp2(t) => (t, b"")
}
let buf = Buffer()
buf.write_bytes(quic_varint_encode(frame_type))
buf.write_bytes(quic_varint_encode(payload.length().to_uint64()))
buf.write_bytes(payload)
buf.to_bytes()
}
///|
/// A varint-prefixed id followed by an opaque block (a PUSH_PROMISE payload).
fn http3_prefixed(id : UInt64, block : Bytes) -> Bytes {
let buf = Buffer()
buf.write_bytes(quic_varint_encode(id))
buf.write_bytes(block)
buf.to_bytes()
}
///|
/// Encode a SETTINGS payload: each pair as its identifier varint then its value varint
/// (RFC 9114 §7.2.4).
pub fn http3_settings_encode(pairs : Array[(UInt64, UInt64)]) -> Bytes {
let buf = Buffer()
for pair in pairs {
buf.write_bytes(quic_varint_encode(pair.0))
buf.write_bytes(quic_varint_encode(pair.1))
}
buf.to_bytes()
}
///|
/// 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).
pub fn http3_frame_decode(
input : BytesView,
) -> (Http3Frame, Int)? raise Http3FrameError {
let (frame_type, type_len) = match quic_varint_decode(input) {
Some(v) => v
None => return None
}
let rest = input[type_len:]
let (length, len_len) = match quic_varint_decode(rest) {
Some(v) => v
None => return None
}
let payload_len = length.to_int()
let header_len = type_len + len_len
if input.length() < header_len + payload_len {
return None
}
let payload = input[header_len:header_len + payload_len].to_owned()
let consumed = header_len + payload_len
let frame = http3_frame_from_payload(frame_type, payload)
Some((frame, consumed))
}
///|
/// Build the typed frame from its type code and its already-sliced payload.
fn http3_frame_from_payload(
frame_type : UInt64,
payload : Bytes,
) -> Http3Frame raise Http3FrameError {
if frame_type == h3_frame_data {
Data(payload)
} else if frame_type == h3_frame_headers {
Headers(payload)
} else if frame_type == h3_frame_cancel_push {
CancelPush(http3_one_varint(payload, "CANCEL_PUSH"))
} else if frame_type == h3_frame_settings {
Settings(http3_settings_decode(payload))
} else if frame_type == h3_frame_push_promise {
let (id, id_len) = http3_lead_varint(payload, "PUSH_PROMISE")
PushPromise(id, payload[id_len:payload.length()].to_owned())
} else if frame_type == h3_frame_goaway {
GoAway(http3_one_varint(payload, "GOAWAY"))
} else if frame_type == h3_frame_max_push_id {
MaxPushId(http3_one_varint(payload, "MAX_PUSH_ID"))
} else if h3_is_reserved_http2(frame_type) {
ReservedHttp2(frame_type)
} else {
Reserved(frame_type, payload)
}
}
///|
/// 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.
pub fn http3_settings_decode(
payload : Bytes,
) -> Array[(UInt64, UInt64)] raise Http3FrameError {
let pairs : Array[(UInt64, UInt64)] = []
let view = payload[:]
let mut off = 0
let n = view.length()
while off < n {
let (id, id_len) = match quic_varint_decode(view[off:]) {
Some(v) => v
None => raise Http3FrameError("truncated SETTINGS identifier")
}
off += id_len
let (value, val_len) = match quic_varint_decode(view[off:]) {
Some(v) => v
None => raise Http3FrameError("truncated SETTINGS value")
}
off += val_len
pairs.push((id, value))
}
pairs
}
///|
/// A payload that is exactly one varint (CANCEL_PUSH / GOAWAY / MAX_PUSH_ID).
fn http3_one_varint(
payload : Bytes,
name : String,
) -> UInt64 raise Http3FrameError {
let (value, _) = http3_lead_varint(payload, name)
value
}
///|
/// The leading varint of a payload plus the octets it consumed.
fn http3_lead_varint(
payload : Bytes,
name : String,
) -> (UInt64, Int) raise Http3FrameError {
match quic_varint_decode(payload[:]) {
Some(v) => v
None => raise Http3FrameError(name + " frame missing its varint field")
}
}
///|
/// 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.
pub fn http3_frame_decode_all(
input : Bytes,
) -> (Array[Http3Frame], Int) raise Http3FrameError {
let frames : Array[Http3Frame] = []
let mut off = 0
let view = input[:]
for ;; {
match http3_frame_decode(view[off:]) {
Some((frame, consumed)) => {
frames.push(frame)
off += consumed
}
None => break
}
}
(frames, off)
}