h2-ws-client 0.1.0

Minimal HTTP/2 WebSocket client built on hyper + tokio-tungstenite
Documentation
# h2-ws-client

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.

---

## Compatibility

This crate is designed to work with servers that implement  
[RFC 8441: Bootstrapping WebSockets with HTTP/2](https://datatracker.ietf.org/doc/html/rfc8441).

- axum (via `WebSocketUpgrade`) already supports WebSockets over HTTP/2
  using the extended `CONNECT` + `:protocol = "websocket"` flow.
- reqwest is an HTTP client and does **not** provide any WebSocket
  abstraction (neither over HTTP/1.1 nor HTTP/2). If you need WebSockets,
  you typically combine `hyper` + `tokio-tungstenite` (or a crate like this one).

---

## How RFC 8441 WebSockets Work

RFC 8441 defines how to bootstrap WebSockets over **HTTP/2** using an
*extended CONNECT* request:

1. The client sends an HTTP/2 request:

   - `:method = CONNECT`  
   - `:protocol = "websocket"`  
   - `:path = "/your-endpoint"`  
   - `sec-websocket-version = 13` (and optionally `sec-websocket-protocol`)

2. If the server accepts, it returns a successful `2xx` response.  
   From this point on, that **single HTTP/2 stream** becomes a
   bidirectional byte stream.

3. Over that byte stream, both sides speak **normal WebSocket frames**
   as defined in [RFC 6455]https://datatracker.ietf.org/doc/html/rfc6455
   (text, binary, ping/pong, close, masking, etc.).
   
diagram description of (3)
<img width="767" height="581" alt="image" src="https://github.com/user-attachments/assets/6f2b924a-b613-4706-a952-36a7d9bdba94" />


So conceptually:

- **WebSocket over HTTP/1.1**  
  - Uses `GET ... HTTP/1.1` + `Upgrade: websocket`  
  - After `101 Switching Protocols`, the whole TCP connection is “taken over” by WebSocket.

- **WebSocket over HTTP/2 (RFC 8441)**  
  - Uses `CONNECT` + `:protocol = "websocket"`  
  - Only **one HTTP/2 stream** (not the entire connection) is used as the underlying WebSocket channel.  
  - Other HTTP/2 streams can still be used for normal HTTP traffic.

This crate only cares about the **client side** of that HTTP/2 WebSocket flow.

---


## Running tests and examples

### 1. Run tests

```bash
cargo test
```


This will run both unit tests and integration tests (for example, against an axum echo server).

#### 2-1. Run the example server

```bash
# In one terminal: run your server (axum / hyper / etc.)
# For example:
cargo run --bin server
```

#### 2-2. Run the example client

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):

```bash
# 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.

---

## Example: Interactive HTTP/2 WebSocket client

```rust
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(())
}
```