WebSockets, WebTransport, SSE, and JSON
A few client capabilities live behind cargo features, so they only pull in their dependencies when you ask for them.
WebSocket client
With the websockets feature, a built conn can be upgraded to a WebSocket. This works over HTTP/1.1 (RFC 6455) and, when the connection negotiated h2, over HTTP/2 extended CONNECT (RFC 8441) — the same upgrade either way from the caller's side.
let ws_conn = client
.get("wss://example.com/ws")
.into_websocket()
.await
.unwrap();
The resulting WebSocketConn exposes the same send/receive interface as the server-side WebSocket handler.
WebTransport client
WebTransport is a protocol over HTTP/3 and QUIC offering multiplexed streams and unreliable datagrams. With the webtransport feature, a client built with new_with_quic can open sessions to a WebTransport server.
Client::webtransport(url) builds a conn preconfigured for the extended-CONNECT handshake — method CONNECT, the :protocol pseudo-header set to webtransport, pinned to HTTP/3. Awaiting it with Conn::into_webtransport() completes the upgrade and hands back a WebTransportConnection, the same session type the server handler uses.
use trillium_client::Client;
use trillium_quinn::ClientQuicConfig;
use trillium_rustls::RustlsConfig;
use trillium_tokio::ClientConfig;
let client = Client::new_with_quic(
RustlsConfig::<ClientConfig>::default(),
ClientQuicConfig::with_webpki_roots(),
);
let conn = client.webtransport("https://example.com/wt");
// let session = conn.into_webtransport().await?;
Multiple sessions to the same origin coalesce onto a single underlying QUIC connection, matching how HTTP/3 request multiplexing already works.
Server-Sent Events
Server-Sent Events is a one-way stream of text events over an ordinary HTTP response — the server holds the response open and writes data:/event:/id: lines as things happen. Unlike WebSockets and WebTransport, there is no protocol upgrade; it works the same over HTTP/1.1, HTTP/2, and HTTP/3.
With the sse feature, Conn::into_sse() sends the request, checks for a success status and a text/event-stream content-type, and hands back an EventStream — a Stream of Events. Note that into_sse() is the execution, so build the conn but don't await it yourself first.
use futures_lite::StreamExt;
use trillium_client::Client;
use trillium_smol::ClientConfig;
let mut events = client
.get("https://example.com/events")
.into_sse()
.await
.unwrap();
while let Some(event) = events.next().await {
let event = event.unwrap();
println!("{}", event.data());
}
Each Event exposes data(), event_type() (None for the default message type), id(), and retry(). The stream yields Result items because reading the underlying connection can fail mid-stream; it ends when the connection closes. This is a single-response stream — it does not reconnect on its own. If you need the browser EventSource's automatic reconnection (re-issuing the request with Last-Event-ID), build that on top, or drive the whole request through a retrying ClientHandler. On failure, into_sse() returns an SseError you can dereference as a Conn to inspect the response — for example, to read an error body returned with a non-2xx status.
JSON bodies
Enabling either the serde_json or sonic-rs feature adds JSON convenience methods backed by that serializer. Conn::response_json::<T>() deserializes a response body, and Conn::with_json_body serializes a request body:
use serde::Deserialize;
use trillium_client::Client;
use trillium_smol::ClientConfig;
#[derive(Deserialize)]
struct Widget {
name: String,
}
let mut conn = client.get("https://api.example.com/widget").await.unwrap();
let widget: Widget = conn.response_json().await.unwrap();
println!("{}", widget.name);
JSON errors surface as ClientSerdeError, which wraps either a transport error or a serializer error. For ad-hoc request bodies without a struct, the crate re-exports a json! macro. The two backends are mutually exclusive — enable one.
Also: gRPC
trillium-grpc builds a spec-conformant gRPC client (and server) on top of trillium-client. You write a .proto, codegen produces a typed <Service>Client wrapping a Client, and each RPC shape — unary, server-streaming, client-streaming, bidirectional — gets a call handle that fits it. See its documentation for the full guide.