http: HTTP/2 frames (step 2 of #72) #685

Closed
opened 2026-10-30 08:26:12 +00:00 by orion · 6 comments
Collaborator

Goal

Step 2 of #72 (HTTP/2): the frame layer of RFC 9113 sections 4 to 6 as an encoder and decoder of its own in std/http, with no connection, no streams state machine and no change to what a Client or a Server does. Step 3 (the connection core) reads and writes frames through it. HPACK (step 1, #563) has landed; header blocks stay opaque octets here.

Rules

A new module std/http/src/h2frame.trb (internal to the package, imported by relative path like hpack.trb):

  • The connection preface (section 3.4): the 24 octets PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n as a constant plus isPreface(buffer) for the server's detection on cleartext (prefix match of what has arrived so far: yes, no, or need more).
  • The frame header (section 4.1): length (24 bits), type, flags, reserved bit, stream identifier (31 bits). FrameHeader encodes and decodes it; a reserved bit set on read is ignored, written as 0.
  • The ten frame types (section 6) as one Frame variant: DATA (with padding), HEADERS (padding, priority fields parsed and kept as PriorityFields but ignored, as decided in the epic), PRIORITY, RST_STREAM, SETTINGS (with ack), PUSH_PROMISE (decoded so a violation can be reported, never encoded), PING (with ack), GOAWAY (last stream, error code, debug data), WINDOW_UPDATE, CONTINUATION. Unknown frame types decode to Unknown(type, flags, streamId, payload) and are to be ignored by the connection (section 4.1).
  • Settings and error codes (sections 6.5.2, 7): SettingsId with the six defined identifiers (unknown ones are kept and ignored), ErrorCode with the fourteen codes (an unknown code is kept as its number).
  • FrameDecoder (of(maxFrameSize)): decode(buffer): Result<Option<Frame>, FrameError> takes what has arrived, returns the next complete frame and consumes it, or None when more octets are needed; it never allocates for a length beyond maxFrameSize (refused as soon as the header is read, FRAME_SIZE_ERROR). Refused with the error code section 4.2, 6 and 7 name: a frame of the wrong size for its type (PING not 8, RST_STREAM not 4, WINDOW_UPDATE not 4, PRIORITY not 5, SETTINGS not a multiple of 6 or an ack with a payload), a stream identifier where the type forbids one or demands one (SETTINGS, PING, GOAWAY on stream 0; DATA, HEADERS, PRIORITY, RST_STREAM, CONTINUATION off stream 0), padding that is as long as or longer than the payload, a SETTINGS value out of range (ENABLE_PUSH not 0 or 1, INITIAL_WINDOW_SIZE above 2^31-1, MAX_FRAME_SIZE outside 16384 to 16777215), a WINDOW_UPDATE of 0 (reported as the error the connection or the stream turns into: PROTOCOL_ERROR on either). FrameError says whether it is a connection or a stream error and carries the code.
  • FrameEncoder (encode(frame): Buffer<UInt8>): the inverse for every type except PUSH_PROMISE; splitHeaderBlock(streamId, block, maxFrameSize, endStream) returns HEADERS plus CONTINUATION frames with END_HEADERS only on the last. DATA padding is written only when the caller gives a pad length.

Acceptance criteria

std/http/tests/h2frame.test.trb (VM and --native, as the other std/http/tests):

  1. Every frame type round-trips through encoder and decoder (with and without padding and flags where the type has them), and the bytes of at least one example per type are compared with the RFC 9113 text or a worked hex example in the test.
  2. The preface constant is byte for byte the 24 octets; isPreface answers yes, no and need-more for the right prefixes.
  3. Each refusal above is its own test and names the expected error code and level (connection or stream).
  4. Streaming: a byte-by-byte feed of three frames yields the three frames, None in between and nothing lost; a length beyond maxFrameSize is refused on the header alone.
  5. splitHeaderBlock with a block of 3 frames' worth gives HEADERS and two CONTINUATION frames and the flags are right; an Unknown frame passes through decode untouched.
  6. A fixed-seed round trip of at least 200 generated frames through one encoder and decoder.

tools/gates.sh a and b green in forge CI. NETWORK.md section 12 "as built" gets the frame layer and slice 12's row says "Partly done: HPACK (step 1 of #72), frames (step 2)"; FIRST-PARTY.md gap 25 gets the same note (it stays open); skills/ regenerated if the docs change reaches them.

Design record / seed

Implements NETWORK.md slice 12 (HTTP/2). Seed: no (std only, no syntax, no native, no string lookup by the compiler).

Size

One PR, two Conventional Commits: feat(http): HTTP/2 frame header, preface and the simple frame types, then feat(http): HTTP/2 HEADERS, CONTINUATION, SETTINGS and the frame decoder limits (with the docs).

Dependencies

#563 (HPACK, landed). Part of #72; steps 3 to 6 follow just in time.

## Goal Step 2 of #72 (HTTP/2): the frame layer of RFC 9113 sections 4 to 6 as an encoder and decoder of its own in `std/http`, with no connection, no streams state machine and no change to what a `Client` or a `Server` does. Step 3 (the connection core) reads and writes frames through it. HPACK (step 1, #563) has landed; header blocks stay opaque octets here. ## Rules A new module `std/http/src/h2frame.trb` (internal to the package, imported by relative path like `hpack.trb`): - **The connection preface** (section 3.4): the 24 octets `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n` as a constant plus `isPreface(buffer)` for the server's detection on cleartext (prefix match of what has arrived so far: yes, no, or need more). - **The frame header** (section 4.1): length (24 bits), type, flags, reserved bit, stream identifier (31 bits). `FrameHeader` encodes and decodes it; a reserved bit set on read is ignored, written as 0. - **The ten frame types** (section 6) as one `Frame` variant: DATA (with padding), HEADERS (padding, priority fields parsed and kept as `PriorityFields` but ignored, as decided in the epic), PRIORITY, RST_STREAM, SETTINGS (with ack), PUSH_PROMISE (decoded so a violation can be reported, never encoded), PING (with ack), GOAWAY (last stream, error code, debug data), WINDOW_UPDATE, CONTINUATION. Unknown frame types decode to `Unknown(type, flags, streamId, payload)` and are to be ignored by the connection (section 4.1). - **Settings and error codes** (sections 6.5.2, 7): `SettingsId` with the six defined identifiers (unknown ones are kept and ignored), `ErrorCode` with the fourteen codes (an unknown code is kept as its number). - **`FrameDecoder`** (`of(maxFrameSize)`): `decode(buffer): Result<Option<Frame>, FrameError>` takes what has arrived, returns the next complete frame and consumes it, or `None` when more octets are needed; it never allocates for a length beyond `maxFrameSize` (refused as soon as the header is read, `FRAME_SIZE_ERROR`). Refused with the error code section 4.2, 6 and 7 name: a frame of the wrong size for its type (PING not 8, RST_STREAM not 4, WINDOW_UPDATE not 4, PRIORITY not 5, SETTINGS not a multiple of 6 or an ack with a payload), a stream identifier where the type forbids one or demands one (SETTINGS, PING, GOAWAY on stream 0; DATA, HEADERS, PRIORITY, RST_STREAM, CONTINUATION off stream 0), padding that is as long as or longer than the payload, a SETTINGS value out of range (`ENABLE_PUSH` not 0 or 1, `INITIAL_WINDOW_SIZE` above 2^31-1, `MAX_FRAME_SIZE` outside 16384 to 16777215), a WINDOW_UPDATE of 0 (reported as the error the connection or the stream turns into: PROTOCOL_ERROR on either). `FrameError` says whether it is a connection or a stream error and carries the code. - **`FrameEncoder`** (`encode(frame): Buffer<UInt8>`): the inverse for every type except PUSH_PROMISE; `splitHeaderBlock(streamId, block, maxFrameSize, endStream)` returns HEADERS plus CONTINUATION frames with END_HEADERS only on the last. DATA padding is written only when the caller gives a pad length. ## Acceptance criteria `std/http/tests/h2frame.test.trb` (VM and `--native`, as the other `std/http/tests`): 1. Every frame type round-trips through encoder and decoder (with and without padding and flags where the type has them), and the bytes of at least one example per type are compared with the RFC 9113 text or a worked hex example in the test. 2. The preface constant is byte for byte the 24 octets; `isPreface` answers yes, no and need-more for the right prefixes. 3. Each refusal above is its own test and names the expected error code and level (connection or stream). 4. Streaming: a byte-by-byte feed of three frames yields the three frames, `None` in between and nothing lost; a length beyond `maxFrameSize` is refused on the header alone. 5. `splitHeaderBlock` with a block of 3 frames' worth gives HEADERS and two CONTINUATION frames and the flags are right; an `Unknown` frame passes through decode untouched. 6. A fixed-seed round trip of at least 200 generated frames through one encoder and decoder. `tools/gates.sh a` and `b` green in forge CI. NETWORK.md section 12 "as built" gets the frame layer and slice 12's row says "Partly done: HPACK (step 1 of #72), frames (step 2)"; FIRST-PARTY.md gap 25 gets the same note (it stays open); `skills/` regenerated if the docs change reaches them. ## Design record / seed Implements NETWORK.md slice 12 (HTTP/2). Seed: no (std only, no syntax, no native, no string lookup by the compiler). ## Size One PR, two Conventional Commits: `feat(http): HTTP/2 frame header, preface and the simple frame types`, then `feat(http): HTTP/2 HEADERS, CONTINUATION, SETTINGS and the frame decoder limits` (with the docs). ## Dependencies #563 (HPACK, landed). Part of #72; steps 3 to 6 follow just in time.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T11:26:25Z

[loop:torbscript] claim implement by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T11:26:25Z
Author
Collaborator

[loop:torbscript] release: the worker hit its 30-minute run limit before pushing anything (no branch, no PR). The item stays ready; the next worker gets a longer run limit.
next: ready

[loop:torbscript] release: the worker hit its 30-minute run limit before pushing anything (no branch, no PR). The item stays ready; the next worker gets a longer run limit. next: ready
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T11:59:40Z

[loop:torbscript] claim implement by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T11:59:40Z
Author
Collaborator

[loop:torbscript] release #685 by session:loop-torbscript-20261030: PR #689 ready for review (head 32dc97a2), argus requested; CI pending.
next: waiting (CI and Argus review on #689)

[loop:torbscript] release #685 by session:loop-torbscript-20261030: PR #689 ready for review (head 32dc97a2), argus requested; CI pending. next: waiting (CI and Argus review on #689)
Author
Collaborator

[loop:torbscript] claim implementation (CI fix) by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T13:39:07Z
PR #689: CI gate format --check fails on std/http/tests/h2frame.test.trb only (1 of 1612 files); a worker formats it and force-pushes.
next: waiting (format fix on #689)

[loop:torbscript] claim implementation (CI fix) by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T13:39:07Z PR #689: CI gate format --check fails on std/http/tests/h2frame.test.trb only (1 of 1612 files); a worker formats it and force-pushes. next: waiting (format fix on #689)
Author
Collaborator

[loop:torbscript] release implementation (CI fix) by session:loop-torbscript-20261030: the format fix is on loop/685-h2-frames as 59fdb46d (style(http): format h2frame tests, only std/http/tests/h2frame.test.trb); torb format --check on it reports 0 of 1 file would change. CI reruns on the new head.
next: waiting (CI and Argus review on #689)

[loop:torbscript] release implementation (CI fix) by session:loop-torbscript-20261030: the format fix is on loop/685-h2-frames as 59fdb46d (style(http): format h2frame tests, only std/http/tests/h2frame.test.trb); torb format --check on it reports 0 of 1 file would change. CI reruns on the new head. next: waiting (CI and Argus review on #689)
argus closed this issue 2026-10-30 13:06:15 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#685
No description provided.