WebSocket
Crate hopf-websocket: RFC 6455 framing and HTTP upgrade helpers on top of hopf-http upgrade seams. Supports HTTP/1.1 101 Upgrade, HTTP/2 Extended CONNECT (RFC 8441), and HTTP/3 Extended CONNECT (RFC 9220).
The crate is server-centric (factory as ServerHandlerFactory); handshake helpers also exist for clients. Framing is push-incremental and split-feed safe.
Contents
Standards
| Reference | Role |
|---|---|
| RFC 6455 | Framing, opcodes, masking, control-frame rules, Accept GUID |
| RFC 8441 | HTTP/2 Extended CONNECT |
| RFC 9220 | HTTP/3 Extended CONNECT |
Constants: WEBSOCKET_GUID = 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, WEBSOCKET_VERSION = 13, MAX_CONTROL_PAYLOAD = 125.
Architecture
HTTP Stream (H1 / H2 / H3)
│ upgrade() / Extended CONNECT
▼
WsUpgradeHandler (ProtocolUpgradeHandler)
│
├── WsFrameParser (role-aware masking)
└── WsEventHandler callbacks
│
└── WsSession send_* helpers
Wire as a normal HTTP ServerHandlerFactory:
use hopf_websocket::{EchoFactory, WebSocketConfig, WebSocketFactory};
let events = EchoFactory;
let ws = WebSocketFactory::new(events, WebSocketConfig::default());
// use `ws` as Arc<dyn ServerHandlerFactory> under CleartextHttpEndpoint / AlpnHttpEndpointHandshake
HTTP/1.1
Helpers: is_h1_websocket_upgrade, validate_h1_upgrade → key, calculate_accept, websocket_accept_headers, generate_key / create_upgrade_request / validate_upgrade_response / WebSocketOpening (client).
Successful upgrade yields 101 with Sec-WebSocket-Accept. Clients must run validate_upgrade_response (or WebSocketOpening::validate_response) on that 101 before installing WsUpgradeHandler::client.
HTTP/2 / HTTP/3
is_extended_connect_websocket / websocket_connect_response_headers — successful Extended CONNECT yields 200 and then WebSocket frames on the stream.
Optional WebSocketConfig.subprotocol is echoed only when that token appears in the client's Sec-WebSocket-Protocol offer; otherwise it is omitted.
Origin (RFC 6455 §10.2): default OriginPolicy::AllowList([]) rejects any present Origin with 403 (browsers always send it). Missing Origin is allowed for native clients. Configure with WebSocketConfig::with_allowed_origins([...]) or allow_any_origin() for demos.
Framing
| Type | Role |
|---|---|
WsFrameParser |
Incremental parser; receive(&[u8], &mut dyn WsFrameHandler) |
WsFrameHandler |
Frame callbacks |
Opcode |
Text / Binary / Close / Ping / Pong / Cont |
WsRole |
Server / Client (masking direction) |
write_frame |
Encode a frame into a Vec<u8> |
WsFrameError |
Protocol / size / UTF-8 errors |
Role / masking:
- Server: inbound frames must be masked; outbound must not.
- Client: reverse (outbound masked via
getrandom).
Enforced: RSV bits zero (no extensions), known opcodes, control-frame constraints (FIN, ≤125 bytes), max_payload, valid UTF-8 for text. Default auto-pong on ping.
Configuration
WebSocketConfig (Default)
| Property | Type | Default | Notes |
|---|---|---|---|
max_payload |
size | 16 MiB | Reject larger data frames |
subprotocol |
Option<String> |
None |
Offered / selected subprotocol |
origin |
OriginPolicy |
Empty AllowList |
Browser Origin must match the list (else 403); missing Origin allowed. Use with_allowed_origins or allow_any_origin |
WsUpgradeHandler
| Constructor | Notes |
|---|---|
WsUpgradeHandler::server(event, max_payload) |
Server role |
WsUpgradeHandler::client(event, max_payload) |
Client role |
Cargo features
| Feature | Effect |
|---|---|
| (default) | Framing + H1/H2 upgrade |
integration |
Enables hopf-http/h3 for H3 tests |
Event handler SPI
pub trait WsEventHandlerFactory {
fn create(&self, path: &str, req: &Headers, conn: ConnHandle) -> Box<dyn WsEventHandler>;
}
pub trait WsEventHandler {
fn opened(&mut self, session: &mut WsSession, conn: &ConnHandle);
fn text_message(&mut self, session: &mut WsSession, text: &str);
fn binary_message(&mut self, session: &mut WsSession, data: &[u8]);
fn ping(&mut self, session: &mut WsSession, payload: &[u8]); // default auto-pong
fn pong(&mut self, session: &mut WsSession, payload: &[u8]);
fn closed(&mut self, code: Option<u16>, reason: &str);
fn error(&mut self, err: WsFrameError);
}WebSocketFactory<F> implements ServerHandlerFactory for any F: WsEventHandlerFactory.
conn is a cloneable hopf_core::ConnHandle for this connection, handed to both create and opened — capture it to hop work back onto this connection's reactor from another thread (e.g. a pub/sub fan-out delivering a message published on a different connection; see hopf-mqtt's WebSocket bridge for a worked example). A plain ConnHandle::send writes straight to the raw transport, bypassing WS framing — wrap the handle with framed_ws_conn_handle (below) before handing it to code that delivers asynchronously; synchronous replies from within a callback should keep using WsSession::send_* instead.
Session send API
impl WsSession {
fn send_text(&mut self, text: &str);
fn send_binary(&mut self, data: &[u8]);
fn send_ping(&mut self, payload: &[u8]);
fn send_pong(&mut self, payload: &[u8]);
fn send_close(&mut self, code: u16, reason: &str);
}Free helpers: write_text, write_binary, write_ping, write_pong, write_close (encode into buffers).
Framing a ConnHandle for async delivery
pub fn framed_ws_conn_handle(conn: &ConnHandle, role: WsRole) -> ConnHandle;Wraps conn so every ConnHandle::send first frames the payload as a WebSocket binary message for role, using hopf_core::ConnHandle::framed (a small additive transform seam on ConnHandle for exactly this case — a protocol layered on top of the raw transport that still needs asynchronous, cross-connection deliveries framed correctly). Use the wrapped handle — not the raw one from create / opened — anywhere a ConnHandle is handed to code that delivers from another connection or thread.
Echo helper
| Type | Role |
|---|---|
EchoFactory |
WsEventHandlerFactory for demos |
EchoWsHandler |
Echoes text/binary |
Used by examples/websocket.
Examples
See Cookbook: WebSocket.
cargo run -p websocket -- 127.0.0.1:8080| Package | Demonstrates | Knobs |
|---|---|---|
examples/websocket |
Echo server | arg1 = listen addr |
No environment variables.
Limitations
- No
permessage-deflateor other extensions (RSV must be zero). - Close handling sends a close and marks the session dead; protocol errors emit Close 1002/1007/1009 then tear down the transport/stream.
- Client opening helpers (
WebSocketOpening,validate_upgrade_response) cover RFC 6455 §4.1 step 5; the stock factory path remains server-oriented. - Relies on HTTP trailers / Extended CONNECT behaviour of
hopf-httpfor H2/H3.