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}