Design (as agreed in the v0.4 pass):
- Dep #3 spent: sha1_smol 1.0.1 (zero transitive deps) instead of a
vendored SHA-1 — user call: no hand-rolled crypto. Consequence noted
in ROADMAP: the v0.6 wire-format JSON question now costs dep #4.
- Base64 stays in-tree but ENCODE-ONLY (RFC 4648 vectors tested): it is
an encoding, not crypto, and the decode direction (where parsing bugs
live) is deliberately not implemented.
- src/ws/{mod,handshake}.rs: validate() implements §4.2.1 (GET, 1.1,
Host, upgrade/connection token lists case-insensitively, key shape =
base64 of exactly 16 bytes, version 13). Version mismatch -> 426 +
sec-websocket-version: 13 (§4.2.2); everything else -> 400. Rejected
upgrades stay plain HTTP with keep-alive intact (tested: 400 then 101
on the same connection).
- Conn grows pub upgrade: Option<WsUpgrade> (opaque marker; chunk 3
turns it into the duplex/handler handoff payload — shape deliberately
uncommitted while the handler API is open). Conn::upgrade(self) is the
commit point: 101 + accept key + marker + halt, or the rejection.
- conn_actor: upgrade marker + status 101 -> write head, leave the HTTP
loop. Until chunk 3 that drops the fd (clients see 101 then EOF);
status check is defensive against a post-handler plug clobbering 101.
- serialise_response: 1xx no longer get content-length injected
(RFC 7230 §3.3.2). 204 left alone on purpose — separate conversation.
Accept-key path pinned to the §1.3 worked example end-to-end (unit +
wire). Suite: 34 unit + 33 integration + 2 doc.