Skip to main content

hpx_yawc/
lib.rs

1//! # hpx-yawc
2//!
3//! WebSocket (RFC 6455) with permessage-deflate compression (RFC 7692).
4//! Autobahn compliant. Supports WASM targets.
5//!
6//! # Features
7//! - `axum`: WebSocket extractor for axum
8//! - `zlib`: Advanced compression with window size control
9//! - `simd`: SIMD-accelerated UTF-8 validation
10//!
11//! # Runtime Support
12//!
13//! hpx-yawc is built on tokio's I/O traits but can work with other async runtimes through simple adapters.
14//! While the library uses tokio internally for its codec and I/O operations, you can integrate it with
15//! runtimes like `smol`, `async-std`, or others by implementing trait bridges between their I/O traits
16//! and tokio's `AsyncRead`/`AsyncWrite`.
17//!
18//! See the [client_smol.rs example](https://github.com/longcipher/hpx/tree/main/crates/yawc/examples/client_smol.rs)
19//! for a complete demonstration of using hpx-yawc with the smol runtime.
20//!
21//! # Client Example
22//! ```rust
23//! use futures::{SinkExt, StreamExt};
24//! use hpx_yawc::{WebSocket, frame::OpCode};
25//!
26//! async fn connect() -> hpx_yawc::Result<()> {
27//!     let mut ws = WebSocket::connect("wss://echo.websocket.org".parse()?).await?;
28//!
29//!     while let Some(frame) = ws.next().await {
30//!         match frame.opcode() {
31//!             OpCode::Text | OpCode::Binary => ws.send(frame).await?,
32//!             OpCode::Ping => {
33//!                 // Pong is sent automatically, but ping is still returned
34//!                 // so you can observe it if needed
35//!             }
36//!             _ => {}
37//!         }
38//!     }
39//!     Ok(())
40//! }
41//! ```
42//!
43//! # Protocol Handling
44//!
45//! hpx-yawc automatically handles WebSocket control frames:
46//!
47//! - **Ping frames**: Automatically responded to with pongs, but still returned to your application
48//! - **Pong frames**: Passed through without special handling
49//! - **Close frames**: Automatically acknowledged, then returned before closing the connection
50//!
51//! # Server Example
52//! ```rust
53//! use futures::StreamExt;
54//! use hpx_yawc::WebSocket;
55//! use http_body_util::Empty;
56//! use hyper::{
57//!     Request, Response,
58//!     body::{Bytes, Incoming},
59//! };
60//!
61//! async fn upgrade(mut req: Request<Incoming>) -> hpx_yawc::Result<Response<Empty<Bytes>>> {
62//!     let (response, fut) = WebSocket::upgrade(&mut req)?;
63//!
64//!     tokio::spawn(async move {
65//!         if let Ok(mut ws) = fut.await {
66//!             while let Some(frame) = ws.next().await {
67//!                 // Process frames
68//!             }
69//!         }
70//!     });
71//!
72//!     Ok(response)
73//! }
74//! ```
75
76#![cfg_attr(docsrs, feature(doc_cfg))]
77
78#[doc(hidden)]
79#[cfg(target_arch = "wasm32")]
80mod wasm;
81
82#[doc(hidden)]
83#[cfg(not(target_arch = "wasm32"))]
84mod native;
85
86#[cfg(not(target_arch = "wasm32"))]
87mod compression;
88
89pub mod close;
90#[cfg(not(target_arch = "wasm32"))]
91pub mod codec;
92pub mod frame;
93#[doc(hidden)]
94pub mod mask;
95#[cfg(not(target_arch = "wasm32"))]
96mod stream;
97
98#[cfg(not(target_arch = "wasm32"))]
99pub use native::*;
100use thiserror::Error;
101#[cfg(target_arch = "wasm32")]
102pub use wasm::*;
103
104/// Result type for WebSocket operations.
105pub type Result<T> = std::result::Result<T, WebSocketError>;
106
107/// Errors that can occur during WebSocket operations.
108#[derive(Error, Debug)]
109pub enum WebSocketError {
110    /// Invalid fragment sequence.
111    #[error("Invalid fragment")]
112    #[cfg(not(target_arch = "wasm32"))]
113    InvalidFragment,
114
115    /// Fragmented message timed out.
116    #[error("Fragmented message timed out")]
117    #[cfg(not(target_arch = "wasm32"))]
118    FragmentTimeout,
119
120    /// Payload contains invalid UTF-8.
121    #[error("Invalid UTF-8")]
122    InvalidUTF8,
123
124    /// Continuation frame without initial frame.
125    #[error("Invalid continuation frame")]
126    #[cfg(not(target_arch = "wasm32"))]
127    InvalidContinuationFrame,
128
129    /// HTTP status code not valid for WebSocket upgrade.
130    #[error("Invalid status code: {0}")]
131    #[cfg(not(target_arch = "wasm32"))]
132    InvalidStatusCode(u16),
133
134    /// Missing or invalid "Upgrade: websocket" header.
135    #[error("Invalid upgrade header")]
136    #[cfg(not(target_arch = "wasm32"))]
137    InvalidUpgradeHeader,
138
139    /// Missing or invalid "Connection: upgrade" header.
140    #[error("Invalid connection header")]
141    #[cfg(not(target_arch = "wasm32"))]
142    InvalidConnectionHeader,
143
144    /// Connection has been closed.
145    #[error("Connection is closed")]
146    ConnectionClosed,
147
148    /// Close frame has invalid format.
149    #[error("Invalid close frame")]
150    #[cfg(not(target_arch = "wasm32"))]
151    InvalidCloseFrame,
152
153    /// Close frame contains invalid status code.
154    #[error("Invalid close code")]
155    #[cfg(not(target_arch = "wasm32"))]
156    InvalidCloseCode,
157
158    /// Reserved bits in frame header are not zero.
159    #[error("Reserved bits are not zero")]
160    #[cfg(not(target_arch = "wasm32"))]
161    ReservedBitsNotZero,
162
163    /// Control frame is fragmented.
164    #[error("Control frame must not be fragmented")]
165    #[cfg(not(target_arch = "wasm32"))]
166    ControlFrameFragmented,
167
168    /// Ping frame exceeds 125 bytes.
169    #[error("Ping frame too large")]
170    #[cfg(not(target_arch = "wasm32"))]
171    PingFrameTooLarge,
172
173    /// Frame payload exceeds configured maximum.
174    #[error("Frame too large")]
175    #[cfg(not(target_arch = "wasm32"))]
176    FrameTooLarge,
177
178    /// Sec-WebSocket-Version is not 13.
179    #[error("Sec-Websocket-Version must be 13")]
180    #[cfg(not(target_arch = "wasm32"))]
181    InvalidSecWebsocketVersion,
182
183    /// Invalid frame opcode.
184    #[error("Invalid opcode (byte={0})")]
185    InvalidOpCode(u8),
186
187    /// Missing Sec-WebSocket-Key header.
188    #[error("Sec-WebSocket-Key header is missing")]
189    #[cfg(not(target_arch = "wasm32"))]
190    MissingSecWebSocketKey,
191
192    /// URL scheme is not ws:// or wss://.
193    #[error("Invalid http scheme")]
194    InvalidHttpScheme,
195
196    /// Received compressed frame but compression not negotiated.
197    #[error("Received compressed frame on stream that doesn't support compression")]
198    #[cfg(not(target_arch = "wasm32"))]
199    CompressionNotSupported,
200
201    /// URL parsing error.
202    #[error(transparent)]
203    UrlParseError(#[from] url::ParseError),
204
205    /// I/O error.
206    #[cfg(not(target_arch = "wasm32"))]
207    #[error(transparent)]
208    IoError(#[from] std::io::Error),
209
210    /// Hyper HTTP error.
211    #[cfg(not(target_arch = "wasm32"))]
212    #[error(transparent)]
213    HTTPError(#[from] hyper::Error),
214
215    #[cfg(target_arch = "wasm32")]
216    #[error("js value: {0:?}")]
217    Js(wasm_bindgen::JsValue),
218}
219
220impl WebSocketError {
221    /// Returns `true` if this is a protocol-level error (RFC 6455 violation).
222    #[cfg(not(target_arch = "wasm32"))]
223    pub fn is_protocol_error(&self) -> bool {
224        matches!(
225            self,
226            Self::InvalidFragment
227                | Self::FragmentTimeout
228                | Self::InvalidContinuationFrame
229                | Self::InvalidCloseFrame
230                | Self::InvalidCloseCode
231                | Self::ReservedBitsNotZero
232                | Self::ControlFrameFragmented
233                | Self::PingFrameTooLarge
234                | Self::InvalidOpCode(_)
235                | Self::CompressionNotSupported
236        )
237    }
238
239    /// Returns `true` if this is a handshake error.
240    #[cfg(not(target_arch = "wasm32"))]
241    pub fn is_handshake_error(&self) -> bool {
242        matches!(
243            self,
244            Self::InvalidStatusCode(_)
245                | Self::InvalidUpgradeHeader
246                | Self::InvalidConnectionHeader
247                | Self::InvalidSecWebsocketVersion
248                | Self::MissingSecWebSocketKey
249                | Self::InvalidHttpScheme
250        )
251    }
252
253    /// Returns `true` if the connection is closed.
254    pub fn is_closed(&self) -> bool {
255        matches!(self, Self::ConnectionClosed)
256    }
257
258    /// Returns `true` if this is a data validation error (invalid UTF-8 or size limit).
259    #[cfg(not(target_arch = "wasm32"))]
260    pub fn is_data_error(&self) -> bool {
261        matches!(self, Self::InvalidUTF8 | Self::FrameTooLarge)
262    }
263
264    /// Returns `true` if this wraps an I/O error.
265    #[cfg(not(target_arch = "wasm32"))]
266    pub fn is_io_error(&self) -> bool {
267        matches!(self, Self::IoError(_))
268    }
269
270    /// Returns the underlying I/O error, if any.
271    #[cfg(not(target_arch = "wasm32"))]
272    pub fn as_io_error(&self) -> Option<&std::io::Error> {
273        match self {
274            Self::IoError(e) => Some(e),
275            _ => None,
276        }
277    }
278}