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.

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 / AlpnHttpEndpoint

Handshake

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:

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