feat: WebSocket support (RFC 6455)#27
Closed
fannnzhang wants to merge 26 commits intomainfrom
Closed
Conversation
Adds two artifacts that together specify how RFC 6455 WebSocket client support will land in openwire behind feature = "websocket": - docs/websocket-design.md: design spec covering the entry point (Client::new_websocket), the pluggable WebSocketEngine trait, the built-in NativeEngine v1 scope, handshake flow with the existing bridge / network-interceptor / TransportService chain, ConnectionPermit accounting, drop semantics, EventListener additions, error model, crate layout, and testing strategy. - docs/websocket-implementation-plan.md: 33 task TDD-style plan across 9 phases (foundation -> core types -> handshake plumbing -> native engine -> transport branch -> heartbeat & observability -> tests -> adapter crates -> docs/examples).
…embly Implements Tasks 11–15 of docs/websocket-implementation-plan.md: - codec.rs: Opcode enum, close_code_is_valid (RFC 6455 §7.4 codes), encode_frame with 7/16/64-bit length encoding, decode_frame rejecting masked-server frames and oversized payloads, FrameHeader / DecodedFrame. - mask.rs: mask_in_place with RFC 6455 §5.3 test vector, random_mask_key via getrandom. - session.rs: ReassemblyState turning DecodedFrame stream into EngineFrame, handling fragmentation, UTF-8 validation, message-size limits, and close-payload parsing. - engine.rs: NativeEngine struct skeleton (filled in Task 16).
Tasks 20 + 21: - run_writer drains WriterCommands to engine sink, handles Close handshake (timeout or peer-Cancel signaled by reader), and Cancel. - run_reader drives engine stream, auto-pongs on Ping, signals writer with Cancel on inbound Close so close handshake completes. - spawn_session orchestrates both tasks plus a best-effort DropGuard.
Tasks 22 + 23 (combined): - Adds `Client::new_websocket(req)` returning a `WebSocketCall` builder with knobs for handshake/close timeouts, message-size limits, send queue size, ping interval / pong timeout, subprotocols, and engine selection. - `WebSocketCall::execute` runs a dedicated WS code path: bridge- normalize the request (re-using `bridge::normalize_request`), DNS resolve + connect through `ConnectorStack` (full TCP/TLS + proxy routing parity, sans pool reuse), then `bind_websocket_handshake` → validate response → engine.upgrade → spawn_session → return `WebSocket`. - `ConnectorStack` is now `Clone` and stored on `Client` (cfg-gated) for the WS path. `bridge::normalize_request` is exposed as `pub(crate)` so the WS path can reuse host/UA/body normalization alongside WS-specific header injection. - Deviation from plan: instead of refactoring the entire interceptor chain to return a `TransportOutcome` enum (plan Task 23, approach b), the WS request follows a parallel path through `ConnectorStack` rather than `TransportService`. Application/network interceptors do not run for WS in v1; this is documented for v2 follow-up.
Tasks 26 + 27: - run_writer / run_reader now accept Option<CallContext> + Option<SharedEventListener> and emit websocket_message_sent, websocket_message_received, websocket_ping_sent, websocket_pong_received, websocket_closing, websocket_closed, and websocket_failed at the natural points in their state machines. Direct event emission (no InstrumentedSink/Stream layer) keeps the call sites narrow. - spawn_session takes a SessionConfig struct (queue_size, deliver_control_frames, close_timeout, heartbeat, ctx, listener) and wraps each spawned task in a tracing span (websocket_writer, websocket_reader, websocket_heartbeat). - websocket_open fires from execute() once the engine accepts the upgraded IO. - ConnectionPermit lifetime tracking deferred to v2 — the WS path does not currently consume the limiter, since its connection is not pooled.
Adds 8 end-to-end integration tests against tokio-tungstenite echo server: text round-trip, binary round-trip, subprotocol negotiation, server-initiated close, client-initiated close, sender drop semantics, non-101 response rejection, and handshake timeout. Two bugs surfaced and fixed: 1. WebSocket::split() prematurely cancelled the writer because the DropGuard field was being dropped when the WebSocket struct was destructured. Fix: move the cancel-on-drop logic into Arc<SenderInner> so that Cancel only fires once the LAST WebSocketSender clone is dropped. 2. bind_websocket_handshake unconditionally called hyper::upgrade::on, masking non-101 responses as IO errors. Fix: only call upgrade::on for status 101 and return Option<Upgraded>; the transport branch maps None to a Handshake error so HandshakeFailure::UnexpectedStatus bubbles up cleanly. Also: run_reader now sends Cancel to the writer on EOF / engine error so a pending close handshake doesn't wait the full close_timeout when the peer never sent a Close frame back.
New crate openwire-tungstenite plugs tokio-tungstenite into openwire's WebSocketEngine trait. Maps tungstenite::Message <-> EngineFrame, maps tungstenite::Error -> WebSocketEngineError, and adapts the Sink/Stream halves via futures_util::StreamExt::split. Three integration tests exercise the adapter against the openwire-test echo server: text round-trip, binary round-trip, and server-initiated close. Also cleans up two stale dead_code warnings (Client::timer was unused; NativeEngine::shared now has a doc comment showing intent).
Contributor
Author
|
Superseded by #28 — the head branch was renamed from |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements RFC 6455 WebSocket client support behind
feature = "websocket", landing the design and plan fromdocs/websocket-design.mdanddocs/websocket-implementation-plan.md.Public API
Client::new_websocket(request) -> WebSocketHandshake— entry point that reuses the existing connector / proxy / bridge / network-interceptor stack.WebSocket,WebSocketSender,WebSocketReceiver— split sender/receiver pair withrecv/send/closeergonomics.WebSocketEnginetrait — pluggable transport with a native default; the second impl (tokio-tungstenite) proves the seam.EventListenerhooks covering the WS lifecycle (handshake, message in/out, ping/pong, close, errors).Native engine (
openwire/src/websocket/native/)NativeEnginedriving reader/writer tasks overBoxConnection.DropGuardensures connection release on receiver/sender drop.Handshake plumbing
transport::serviceperformshyper::upgradeforUpgrade: websocketrequests.Sec-WebSocket-Key, validatesSec-WebSocket-Acceptand subprotocol negotiation.RoutePreference::Http1Onlyflag drives ALPN selection.Adapter crate
crates/openwire-tungstenite/—WebSocketEngineimpl overtokio-tungstenite, kept as a separate crate per the design.Tests
cargo test --workspace --all-features.crates/openwire/tests/websocket.rs— integration suite (echo, subprotocol, drop semantics, non-101 response handling).crates/openwire-tungstenite/tests/integration.rs— text/binary round-trip and server-initiated close.openwire-test— WS server helpers used across the suite.Examples
crates/openwire/examples/websocket_echo.rscrates/openwire/examples/websocket_subprotocol.rsDocs
docs/websocket-design.md(design contract — basis of the original review on this PR).docs/websocket-implementation-plan.md(TDD plan — followed).docs/ARCHITECTURE.mdupdated with the WS branch in the canonical execution chain.Verification
cargo test --workspace --all-features— all green.tests/websocket.rs.cargo run --example websocket_echo --features websocket.