Files
boxmaker/docs/plans/M4a/08-gatewayd-ws-frames.md
T
kyleandClaude Opus 5.5 0339dc13b2 Plan M4a: gatewayd in 15 tasks, with skeletons and given tests
Each task's tests were run against a reference at its end state; the end states were replayed
from master in order with the gate at each step (650 to 762 tests); each skeleton compiles
against its tests and fails them. The reference is kept off this machine. Lessons T27 (every
wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:05:44 -07:00

3.9 KiB

M4a task 08: WebSocket frames

Branch: m4a (run git switch m4a; git status --short must be empty, otherwise stop) Commit subject: gatewayd: ws frames, a strict decoder and a masked encoder

Goal

After the handshake, everything is frames (RFC 6455, section 5). This task is the codec, without I/O: a Decoder that is fed bytes as they arrive, in any pieces, and yields whole messages, and encode, which builds our frames. Everything the server sends is untrusted, so the decoder is strict and checks every length before it allocates (spec section 6):

  • A frame from the server that is masked, sets a reserved bit, uses an opcode other than continuation (0), text (1), close (8), ping (9) or pong (10), is a control frame (8 to 10) over 125 bytes or not final, is a continuation with nothing to continue, or a new text frame while a message is unfinished, is a Protocol error.
  • A length not in its shortest form (126 for a length under 126; 127 for one that fits in 16 bits), or a 64-bit length with its top bit set, is a Protocol error.
  • A message over MAX_MESSAGE (1 MiB), counted from the length fields of its frames before their payloads arrive, is TooLarge.
  • Text must be UTF-8 once whole (a character may be split across frames).
  • A close frame's payload is empty, or a 2-byte code and a UTF-8 reason; one byte is an error.

Every frame we send is final, masked with the 4 bytes we are given.

Files

  • Copy: crates/gatewayd/tests/ws_frame.rs, and the skeleton crates/gatewayd/src/ws/frame.rs
  • Modify: crates/gatewayd/src/ws/mod.rs (pub mod frame;), docs/implementer-log.md

The skeleton

Written: MAX_MESSAGE, the opcode constants, Incoming (Text, Ping, Pong, Close(Option<u16>, String)), Decoder { buf, partial } with new and feed, and the private Header { fin, opcode, header_len, payload_len }.

To fill, in this order, each with its steps above it:

  1. Decoder::header(&self) -> Result<Option<Header>, WsError>: the next frame's header once all of it is in buf, checked against every rule above that does not need the payload. Ok(None) means "wait for more bytes". It only reads buf; it removes nothing.
  2. close(payload) -> Result<Incoming, WsError>.
  3. Decoder::next_message(&mut self): uses header, and when the whole frame is in buf, takes it out (drain) and acts on it. Control frames may come between the frames of a text message and are returned at once.
  4. encode(opcode, payload, mask) -> Vec<u8>.

No indexing that can go out of bounds, no as casts: read lengths with get(2..4) and u16::from_be_bytes, and convert with usize::try_from / u8::try_from.

Steps

  • 1. Copy. git switch m4a, then cp docs/plans/M4a/files/crates/gatewayd/tests/ws_frame.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/src/ws/frame.rs crates/gatewayd/src/ws/. Add pub mod frame; to crates/gatewayd/src/ws/mod.rs.
  • 2. See it fail. cargo test -p gatewayd --test ws_frame. Expected: it compiles and 7 tests fail.
  • 3. Fill the four functions, cargo check -p gatewayd after each.
  • 4. See it pass. cargo test -p gatewayd --test ws_frame. Expected: 7 passed. The last test feeds 300 random streams, half of them with bits flipped, in random pieces, and compares your decoder with a simple one written inside the test: they must return the same messages, and both fail or both not.
  • 5. Run the gate. cargo fmt --all, then make gate. Expected last line: gate: ok.
  • 6. Log and commit. git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit

Done when

  • cargo test -p gatewayd --test ws_frame passes; make gate prints gate: ok.

Stop and report if

  • The random-stream test fails on a case you believe the naive decoder gets wrong. Report the seed and the bytes; do not change the test.