| Crates.io | h2-ws-client |
| lib.rs | h2-ws-client |
| version | 0.1.0 |
| created_at | 2025-11-17 15:53:29.67712+00 |
| updated_at | 2025-11-17 15:53:29.67712+00 |
| description | Minimal HTTP/2 WebSocket client built on hyper + tokio-tungstenite |
| homepage | https://github.com/nam2ee/h2-ws-client |
| repository | https://github.com/nam2ee/h2-ws-client |
| max_upload_size | |
| id | 1937061 |
| size | 51,270 |
Minimal client-side WebSocket over HTTP/2, built on top of hyper and tokio-tungstenite.
The goal of this crate is to provide a very small abstraction for WebSocket over HTTP/2 using RFC 8441 “Extended CONNECT”, without trying to be a full-featured WebSocket client.
⚠️ Warning
This is intentionally a minimal implementation.
It is not battle-tested, and using it in production is entirely at your own risk. You may need to add your own error handling, reconnection logic, TLS, timeouts, etc.
This crate is designed to work with servers that implement
RFC 8441: Bootstrapping WebSockets with HTTP/2.
WebSocketUpgrade) already supports WebSockets over HTTP/2
using the extended CONNECT + :protocol = "websocket" flow.hyper + tokio-tungstenite (or a crate like this one).RFC 8441 defines how to bootstrap WebSockets over HTTP/2 using an extended CONNECT request:
The client sends an HTTP/2 request:
:method = CONNECT:protocol = "websocket":path = "/your-endpoint"sec-websocket-version = 13 (and optionally sec-websocket-protocol)If the server accepts, it returns a successful 2xx response.
From this point on, that single HTTP/2 stream becomes a
bidirectional byte stream.
Over that byte stream, both sides speak normal WebSocket frames as defined in RFC 6455 (text, binary, ping/pong, close, masking, etc.).
diagram description of (3)
So conceptually:
WebSocket over HTTP/1.1
GET ... HTTP/1.1 + Upgrade: websocket101 Switching Protocols, the whole TCP connection is “taken over” by WebSocket.WebSocket over HTTP/2 (RFC 8441)
CONNECT + :protocol = "websocket"This crate only cares about the client side of that HTTP/2 WebSocket flow.
cargo test
This will run both unit tests and integration tests (for example, against an axum echo server).
# In one terminal: run your server (axum / hyper / etc.)
# For example:
cargo run --bin server
Assuming you have a compatible server listening on 127.0.0.1:3000 and serving
a WebSocket endpoint at /echo (for example, an axum WebSocketUpgrade handler):
# In another terminal: run the example client
cargo run --bin example_client
You should then be able to type into the client and see the server’s responses.
use futures_util::{SinkExt, StreamExt};
use h2_ws_client::{H2WsConnection, H2WebSocketStream};
use tokio::{
io::{AsyncBufReadExt, BufReader},
spawn,
};
use tokio_tungstenite::tungstenite::Message;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Connect over TCP and perform the HTTP/2 handshake.
let mut conn = H2WsConnection::connect_tcp("127.0.0.1:3000").await?;
// 2. Open a WebSocket over HTTP/2 using RFC 8441 extended CONNECT.
//
// This sends:
// :method = CONNECT
// :protocol = "websocket"
// :path = "/echo"
// Host = "localhost"
// Sec-WebSocket-Version = 13
//
// The server (e.g. axum WebSocketUpgrade) upgrades this single stream
// into a WebSocket-compatible byte stream.
let ws: H2WebSocketStream = conn
.connect_websocket("/echo", "localhost", Some("echo"))
.await?;
println!("[client] WebSocket over HTTP/2 established!");
println!("--- Type anything and press ENTER to send over WebSocket ---");
println!("--- Ctrl+C to quit ---");
// Split the WebSocket into a sender and receiver half so we can
// read from stdin and the socket concurrently.
let (mut ws_tx, mut ws_rx) = ws.split();
// Task 1: read lines from stdin and send them as text messages.
let input_task = spawn(async move {
let mut stdin = BufReader::new(tokio::io::stdin()).lines();
while let Ok(Some(line)) = stdin.next_line().await {
if ws_tx.send(Message::Text(line.into())).await.is_err() {
println!("[client] failed to send (connection closed)");
break;
}
}
});
// Task 2: receive messages from the server and print them.
let output_task = spawn(async move {
while let Some(msg) = ws_rx.next().await {
match msg {
Ok(m) => println!("[server] {m:?}"),
Err(e) => {
println!("[client] receive error: {e}");
break;
}
}
}
});
// Wait until either stdin finishes or the WebSocket is closed.
tokio::select! {
_ = input_task => println!("[client] input task finished"),
_ = output_task => println!("[client] output task finished"),
}
Ok(())
}