# WebTransport

> Browser-compatible W3C WebTransport over HTTP/3 on Tokio or Compio, routed through the Router, plus a raw QUIC session helper on Tokio.

Tako serves W3C WebTransport over HTTP/3, the protocol behind the browser's
`new WebTransport(url)`. A session carries bidirectional and unidirectional
streams plus unreliable datagrams over one QUIC connection. It runs on the
HTTP/3 server on either runtime, behind the `webtransport` feature, which also
enables `http3`.

```toml
[dependencies]
tako-rs = { version = "2", features = ["webtransport"] }
```

## Accepting sessions

A session starts as an HTTP/3 extended CONNECT request. It goes through the
router like any other request, so path matching, extractors, and middleware such
as authentication run first. Register a `CONNECT` route whose handler takes the
`WebTransport` extractor and returns `on_session`:

```rust
use tako::Method;
use tako::TlsCert;
use tako::router::Router;
use tako::types::{BoxError, Response};
use tako::webtransport::{WebTransport, WebTransportSession};
use tokio::io::AsyncWriteExt;

async fn echo(wt: WebTransport) -> Response {
  wt.on_session(|session: WebTransportSession| async move {
    while let Ok(Some(stream)) = session.accept_bi().await {
      tokio::spawn(async move {
        let (mut send, mut recv) = stream.split();
        let _ = tokio::io::copy(&mut recv, &mut send).await;
        let _ = send.shutdown().await;
      });
    }
  })
}

#[tokio::main]
async fn main() -> Result<(), BoxError> {
  let mut router = Router::new();
  router.route(Method::CONNECT, "/echo", echo);

  tako::Server::builder()
    .tls(TlsCert::pem_paths("cert.pem", "key.pem"))
    .build()
    .try_spawn_h3("0.0.0.0:4433", router)?
    .result()
    .await?;
  Ok(())
}
```

`on_session` answers `200 OK`, and the callback runs once the server has sent the
response. To refuse a session, return any non-2xx response instead. The
`WebTransport` extractor rejects requests that are not a WebTransport CONNECT with
`400 Bad Request`, and a path without a CONNECT route answers `404`.

In the browser:

```js
const transport = new WebTransport("https://example.com:4433/echo");
await transport.ready;
const stream = await transport.createBidirectionalStream();
```

## Streams and datagrams

| Method                              | Result                                          |
| ----------------------------------- | ----------------------------------------------- |
| `accept_bi()`                       | The next bidirectional stream the client opens  |
| `accept_uni()`                      | The next unidirectional stream the client opens |
| `open_bi()` / `open_uni()`          | A stream opened by the server                   |
| `read_datagram()`                   | The next datagram of the session                |
| `send_datagram(bytes)`              | Sends an unreliable datagram                    |
| `remote_address()` / `session_id()` | The client and the session                      |

`BidiStream`, `SendStream`, and `RecvStream` implement `AsyncRead` and
`AsyncWrite` from both `tokio::io` and `futures::io`. `BidiStream::split` returns
separate sending and receiving sides; shutting down the sending side finishes the
stream.

`WebTransportSession` is `Clone`, so one task can read datagrams while another
accepts streams. Each QUIC connection carries one session. Other HTTP/3 requests
that arrive on the same connection are still served by the router.

QUIC datagrams are always on with the `webtransport` feature, whatever
`ServerConfig::h3_enable_datagrams` says, because WebTransport requires them.

## Closing a session

A session ends when:

* the browser calls `transport.close({ closeCode, reason })` or goes away;
* the server calls `session.close(code, reason).await`;
* the handler returns, which closes the session with code 0;
* the server shuts down.

After that, `accept_bi`, `accept_uni`, and `read_datagram` return `Ok(None)`, so
accept loops end on their own. `session.closed().await` resolves with a
`WebTransportClose { code, reason }` from whichever side closed first, and the
browser reads the server's code and reason from `transport.closed`.

## Runtimes

<Tabs items="['Tokio','Compio']">
  <Tab value="Tokio">
    The session and its streams are `Send`, so the example above hands each stream to
    `tokio::spawn`.
  </Tab>

  <Tab value="Compio">
    Serve the same router with `CompioServer::builder()` and `try_spawn_h3`. The
    session and its streams are `!Send` and stay on the connection's thread, so spawn
    per-stream work with `compio::runtime::spawn` and use the `futures::io` traits:

    ```rust
    use futures_util::io::AsyncWriteExt;
    use tako::types::Response;
    use tako::webtransport::{WebTransport, WebTransportSession};

    async fn echo(wt: WebTransport) -> Response {
      wt.on_session(|session: WebTransportSession| async move {
        while let Ok(Some(stream)) = session.accept_bi().await {
          compio::runtime::spawn(async move {
            let (mut send, recv) = stream.split();
            let _ = futures_util::io::copy(recv, &mut send).await;
            let _ = send.close().await;
          })
          .detach();
        }
      })
    }
    ```
  </Tab>
</Tabs>

## Certificates for local development

Browsers only connect to a certificate they trust. During development, skip the
certificate store by passing the certificate's SHA-256 hash to
`serverCertificateHashes`:

```js
const transport = new WebTransport("https://127.0.0.1:4433/echo", {
  serverCertificateHashes: [{ algorithm: "sha-256", value: certificateHash }],
});
```

The certificate must be an ECDSA P-256 certificate valid for at most 14 days.
`examples/webtransport` generates one at startup, serves a test page at
`http://127.0.0.1:3000`, and echoes a stream and a datagram:

Source: [examples/webtransport/src/main.rs](https://github.com/rust-dd/tako/blob/main/examples/webtransport/src/main.rs)

```rust
//! W3C WebTransport from a browser, with no certificate to install.
//!
//! Run it and open <http://127.0.0.1:3000> in Chrome, Edge, or Firefox. The
//! page opens a session to `https://127.0.0.1:4433/echo` with
//! `serverCertificateHashes`, which accepts the short-lived self-signed
//! certificate generated at startup, then echoes a stream and a datagram.

use rcgen::CertificateParams;
use rcgen::KeyPair;
use sha2::Digest;
use sha2::Sha256;
use tako::Method;
use tako::TlsCert;
use tako::body::TakoBody;
use tako::router::Router;
use tako::types::BoxError;
use tako::types::Response;
use tako::webtransport::WebTransport;
use tako::webtransport::WebTransportSession;
use time::Duration;
use time::OffsetDateTime;
use tokio::io::AsyncWriteExt;

async fn echo(wt: WebTransport) -> Response {
  wt.on_session(|session: WebTransportSession| async move {
    println!("session from {}", session.remote_address());

    let datagrams = tokio::spawn({
      let session = session.clone();
      async move {
        while let Ok(Some(datagram)) = session.read_datagram().await {
          let _ = session.send_datagram(datagram);
        }
      }
    });
    while let Ok(Some(stream)) = session.accept_bi().await {
      tokio::spawn(async move {
        let (mut send, mut recv) = stream.split();
        let _ = tokio::io::copy(&mut recv, &mut send).await;
        let _ = send.shutdown().await;
      });
    }
    datagrams.abort();

    let close = session.closed().await;
    println!("session closed: code {} ({:?})", close.code, close.reason);
  })
}

/// A self-signed ECDSA P-256 certificate valid for ten days: the kind
/// `serverCertificateHashes` accepts. Returns it with its SHA-256 hash.
fn certificate() -> Result<(TlsCert, Vec<u8>), BoxError> {
  let key = KeyPair::generate()?;
  let mut params = CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()])?;
  params.not_before = OffsetDateTime::now_utc() - Duration::hours(1);
  params.not_after = OffsetDateTime::now_utc() + Duration::days(10);
  let cert = params.self_signed(&key)?;
  let hash = Sha256::digest(cert.der()).to_vec();
  Ok((TlsCert::der(vec![cert.der().clone()], key.into()), hash))
}

#[tokio::main]
async fn main() -> Result<(), BoxError> {
  let (tls, hash) = certificate()?;
  let hash: Vec<String> = hash.iter().map(u8::to_string).collect();
  let page = include_str!("index.html").replace("CERT_HASH", &hash.join(","));

  let mut pages = Router::new();
  pages.get("/", move || {
    let mut page = Response::new(TakoBody::from(page.clone()));
    page.headers_mut().insert(
      tako::header::CONTENT_TYPE,
      tako::header::HeaderValue::from_static("text/html; charset=utf-8"),
    );
    async move { page }
  });
  let mut sessions = Router::new();
  sessions.route(Method::CONNECT, "/echo", echo);

  // Browsers load pages over HTTP/3 only from a trusted certificate, so the
  // page comes over HTTP/1.1; `127.0.0.1` still counts as a secure context.
  let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
  let page_server = tako::Server::builder()
    .build()
    .try_spawn_http(listener, pages)?;
  let webtransport = tako::Server::builder()
    .tls(tls)
    .build()
    .try_spawn_h3("127.0.0.1:4433", sessions)?;

  println!("open http://127.0.0.1:3000");
  tokio::select! {
    result = page_server.result() => result?,
    result = webtransport.result() => result?,
  }
  Ok(())
}
```

## Raw QUIC sessions

`serve_webtransport(addr, cert_path, key_path, handler)` is a separate, lower-level
API. It binds a plain QUIC endpoint and hands each connection to your handler as a
`RawQuicSession`, with no HTTP/3 or WebTransport handshake. Browsers cannot connect
to it; use it for private QUIC tunnels between peers you control. It is
Tokio-only and needs the `webtransport` feature.

Source: [examples/raw-quic/src/main.rs](https://github.com/rust-dd/tako/blob/main/examples/raw-quic/src/main.rs)

```rust
use tako::webtransport::RawQuicSession;
use tako::webtransport::serve_webtransport;

async fn handle_session(session: RawQuicSession) {
  let remote = session.remote_address();
  println!("New WebTransport session from {remote}");

  // Handle bidirectional streams (echo server)
  loop {
    tokio::select! {
      // Accept bidirectional streams
      result = session.accept_bi() => {
        match result {
          Ok((mut send, mut recv)) => {
            println!("[{remote}] New bidirectional stream");
            tokio::spawn(async move {
              let mut buf = vec![0u8; 4096];
              loop {
                match recv.read(&mut buf).await {
                  Ok(Some(n)) => {
                    println!("[stream] Echoing {n} bytes");
                    if send.write_all(&buf[..n]).await.is_err() {
                      break;
                    }
                  }
                  _ => break,
                }
              }
            });
          }
          Err(e) => {
            println!("[{remote}] Connection closed: {e}");
            break;
          }
        }
      }
      // Read unreliable datagrams
      result = session.read_datagram() => {
        match result {
          Ok(data) => {
            println!("[{remote}] Datagram: {} bytes", data.len());
            // Echo datagram back
            let _ = session.send_datagram(data);
          }
          Err(e) => {
            println!("[{remote}] Datagram error: {e}");
            break;
          }
        }
      }
    }
  }

  println!("Session closed: {remote}");
}

#[tokio::main]
async fn main() {
  tracing_subscriber::fmt::init();

  println!("WebTransport echo server on [::]:4433");
  println!("Requires TLS certificates: cert.pem and key.pem");
  println!();
  println!("Generate self-signed certs for testing:");
  println!("  openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \\");
  println!("    -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'");

  serve_webtransport("[::]:4433", "cert.pem", "key.pem", |session| {
    Box::pin(handle_session(session))
  })
  .await;
}
```

`RawQuicSession` wraps a `quinn::Connection` and exposes its stream and datagram
surface directly:

| Method                 | Purpose                                                               |
| ---------------------- | --------------------------------------------------------------------- |
| `remote_address()`     | Peer socket address.                                                  |
| `accept_bi()`          | Accept an incoming bidirectional stream (`SendStream`, `RecvStream`). |
| `accept_uni()`         | Accept an incoming unidirectional stream (`RecvStream`).              |
| `open_bi()`            | Open a new bidirectional stream.                                      |
| `open_uni()`           | Open a new unidirectional stream.                                     |
| `read_datagram()`      | Read an unreliable datagram.                                          |
| `send_datagram(bytes)` | Send an unreliable datagram.                                          |
| `close(code, reason)`  | Close the session with a QUIC error code.                             |
| `connection()`         | Borrow the underlying `quinn::Connection` for advanced use.           |

`serve_webtransport_with_shutdown(addr, cert, key, handler, signal)` adds a
graceful-shutdown future: the endpoint stops accepting, drains in-flight sessions
for up to 30 s, then aborts the rest. The endpoint installs the `ring` rustls
`CryptoProvider` if none is installed yet and advertises the `h3` ALPN protocol.

## Related

* [HTTP/3](/docs/transports/http3): the QUIC transport WebTransport runs on.
* [Feature flags](/docs/reference/features): the `webtransport` feature.

Source: https://tako.rust-dd.com/docs/transports/webtransport