15 WebTransport
spotdemo4 edited this page 2026-07-11 11:35:52 -04:00

WebTransport

TrevRPC supports WebTransport as an alternate transport for the same protobuf wire protocol used by native QUIC. Generated services, clients, call options, metadata, deadlines, statuses, streaming APIs, metrics, and server limits are the same at the TrevRPC layer.

WebTransport support is transport support only. It does not make TrevRPC compatible with gRPC-Web or Connect. Ordinary TrevRPC HTTP/3 POST support is documented separately in HTTP/3.

When To Use It

Use native QUIC when both endpoints can use a native TrevRPC transport, including Rust, Go, C, Node, or Kotlin Netty, and you do not need HTTP/3 WebTransport compatibility.

Use WebTransport when the deployment needs WebTransport infrastructure or browser-style client connectivity. Browser clients can use trevrpc-js, which carries the same TrevRPC frames over the browser WebTransport API.

Transport Matrix

Runtime Native QUIC WebTransport
C trevrpc_channel* over MsQuic Shared MsQuic server listener; advanced raw client APIs in trevrpc_raw.h
Go *trevrpc.Channel from trevrpc.Dial with an address target *trevrpc.Channel from trevrpc.Dial with an https:// target
Rust trevrpc::client::Channel::connect over Quinn Channel::connect_webtransport; h3-webtransport server
Kotlin RpcChannel from NettyRpcChannel.nativeQuic Unified Netty server on the H3 listener; no Kotlin WebTransport client
JavaScript Node native MsQuic channel from root connect or trevrpc-js/node; no browser QUIC Browser Channel from root connect; Node native server delegates WebTransport policy to the C listener

Wire Model

Native QUIC uses TLS ALPN trevrpc/1 and opens one bidirectional QUIC stream per RPC.

WebTransport runs over HTTP/3 and opens one bidirectional WebTransport stream per RPC. The bytes carried inside that stream are the same TrevRPC frames documented in Protocol and Wire Format.

Do not configure WebTransport clients with the TrevRPC native QUIC ALPN. WebTransport uses HTTP/3 negotiation, while native QUIC uses trevrpc/1. Servers that accept both transports on one listener should advertise both ALPN values and dispatch by the negotiated protocol.

Go Server

Go WebTransport support is opt-in through ServerOptions.EnableWebTransport. The same ServeQUIC entry point handles native QUIC connections and, when enabled, HTTP/3 WebTransport connections.

tlsConfig := &tls.Config{
	NextProtos: []string{http3.NextProtoH3},
}

quicConfig := &quic.Config{
	EnableDatagrams:                  true,
	EnableStreamResetPartialDelivery: true,
}

listener, err := quic.ListenAddr("127.0.0.1:50051", tlsConfig, quicConfig)
if err != nil {
	log.Fatal(err)
}
defer listener.Close()

server := trevrpc.NewServer()
greeter.RegisterGreeterServer(server, greeterService{})

options := server.Options()
options.EnableWebTransport = true
options.WebTransportAdmission = func(request trevrpc.WebTransportAdmissionRequest) bool {
	return request.Secure &&
		request.Path == "/trevrpc" &&
		request.Origin == "http://127.0.0.1:8080"
}
server.SetOptions(options)

if err := trevrpc.ServeQUIC(ctx, listener, server); err != nil {
	log.Fatal(err)
}

Go requires ServerOptions.WebTransportAdmission when WebTransport is enabled. The callback receives path, authority, origin, TLS state, and the raw HTTP request; a nil callback rejects the session. There is no path or check-origin fallback.

To serve native QUIC and WebTransport on the same quic-go listener, advertise both ALPN values:

tlsConfig := &tls.Config{
	NextProtos: []string{trevrpc.ALPN, http3.NextProtoH3},
}

Native QUIC clients will negotiate trevrpc/1. WebTransport clients will negotiate HTTP/3.

Go Client

Pass an https:// target to trevrpc.Dial. It returns the same long-lived *trevrpc.Channel used for native QUIC.

channel, err := trevrpc.Dial(
	ctx,
	"https://127.0.0.1:50051/trevrpc",
	trevrpc.DialOptions{
		TLSConfig: tlsConfig,
		QUICConfig: &quic.Config{
			EnableDatagrams:                  true,
			EnableStreamResetPartialDelivery: true,
		},
	},
)
if err != nil {
	log.Fatal(err)
}

client := greeter.NewGreeterClient(
	channel,
	trevrpc.WithTimeout(5*time.Second),
)

The channel configures HTTP/3 ALPN for the WebTransport target. It reconnects future calls, but never retries, replays, resumes, or moves an RPC. trevrpc.Advanced.DialRawWebTransport and trevrpc.Advanced.NewRawWebTransportClient are the advanced single-session APIs.

Rust Server

Rust WebTransport support is behind the webtransport feature, which is enabled by default. Combined servers use h3-webtransport so ordinary HTTP/3 and WebTransport can coexist on one H3 connection. Native WebTransport clients continue to use web-transport-quinn.

server_crypto.alpn_protocols = vec![
    trevrpc::ALPN.to_vec(),
    trevrpc::HTTP3_ALPN.to_vec(),
];

let mut server = trevrpc::server::Server::new();
greeter::register_greeter(&mut server, GreeterService);

trevrpc::quinn::configure_server_config(
    &mut server_config,
    server.options(),
    trevrpc::quinn::TransportMode::WebTransport,
);
let endpoint = quinn::Endpoint::server(server_config, "127.0.0.1:5000".parse()?)?;

server.serve_quinn_and_webtransport(endpoint).await?;

Use serve_quinn_and_webtransport_with_shutdown when you have a shutdown future and want graceful draining. For WebTransport-only serving, create a Quinn endpoint that advertises web_transport_quinn::ALPN, wrap it with web_transport_quinn::Server::new(endpoint), and call serve_webtransport or serve_webtransport_with_shutdown. The standalone path retains the connection-owning web-transport-quinn server.

Rust admission can be configured with ServerOptions::with_webtransport_admission or Server::set_webtransport_admission. When no callback is set, the existing path, authority, and origin allowlist settings are used.

C and Node Native Servers

C high-level trevrpc_server_listen creates a shared native QUIC plus HTTP/3 listener when webtransport_path is set or ordinary HTTP/3 is enabled. The default WebTransport path is /trevrpc. Server-side WebTransport admission uses trevrpc_webtransport_admission, with webtransport_path and webtransport_origin translated into the default callback policy.

Node native NodeServer.listen uses the same C shared-listener model through the native addon. Configure certificate files and local development trust options through Node listen/connect options.

Production WebTransport deployments should install an explicit admission callback or equivalent policy that checks path, authority, origin, and TLS expectations. Default local examples are intentionally permissive enough for development.

Kotlin Server

Kotlin Netty enables WebTransport on the same H3 listener used for ordinary HTTP/3. An explicit admission policy is required; absent admission rejects the CONNECT request.

val listener = NettyRpcServer.bind(
    server,
    NettyRpcServerConfig(
        bindAddress = InetSocketAddress("127.0.0.1", 50051),
        tls = NettyServerTls.Pem(keyFile, certificateFile),
        enableNative = true,
        enableHttp3 = true,
        enableWebTransport = true,
        webTransportAdmission = WebTransportAdmission { request ->
            request.secure &&
                request.path == WEBTRANSPORT_PATH &&
                request.origin == "https://app.example.com"
        },
    ),
)

The server admits at most one WebTransport session per H3 connection and routes WebTransport bidi streams separately from ordinary HTTP/3 request streams. Kotlin does not currently expose a WebTransport client transport.

Rust Client

Create a web_transport_quinn::Client and pass it with the HTTPS origin to the routine channel constructor. The constructor uses /trevrpc.

let webtransport_client = web_transport_quinn::ClientBuilder::new()
    .with_server_certificates(vec![cert_der])?;

let channel = trevrpc::client::Channel::connect_webtransport(
    webtransport_client,
    "https://127.0.0.1:5000",
).await?;
let client = greeter::GreeterClient::new(channel.clone());

The certificate pinning pattern is useful for local self-signed certificates. Production deployments should configure identity and trust policy through Quinn/rustls according to the deployment's certificate model.

Use trevrpc::advanced::connect_webtransport_channel_with_request when a specialized integration needs a custom CONNECT request. trevrpc::advanced::RawWebTransport owns one established session and does not reconnect.

Browser WebTransport certificate hashes only work for short-lived self-signed certificates. The checked-in Go and Rust examples generate local certificates with lifetimes under this browser limit.

Browser clients rely on globalThis.WebTransport and createBidirectionalStream. Runtimes that do not expose those APIs fail fast with TrevRPC Unavailable instead of falling back to Fetch, gRPC-Web, or Connect semantics.

JavaScript Client

Install the runtime package and generate JavaScript TrevRPC bindings with protoc-gen-trevrpc-js. The generated .trevrpc.js file can be used from JavaScript directly, and the companion .trevrpc.d.ts file provides TypeScript types:

import { connect } from "trevrpc-js";
import { GreeterClient } from "./hello/v1/greeter.trevrpc.js";
import type { HelloRequest } from "./hello/v1/greeter.trevrpc.js";

const channel = await connect(
  "https://127.0.0.1:50051/trevrpc",
);
const client = new GreeterClient(channel, { timeoutMs: 5000 });

const request: HelloRequest = { name: "Trev" };
try {
  const reply = await client.sayHello(request);
} finally {
  channel.close({ closeCode: 0, reason: "client shutdown" });
}

Generated JavaScript clients use protobuf.js reflection types and plain JavaScript objects for protobuf messages.

Browser WebTransport constructor options are explicit top-level connect options. For local self-signed examples, pass serverCertificateHashes directly:

const channel = await connect(
  "https://127.0.0.1:50051/trevrpc",
  {
    serverCertificateHashes: [
      {
        algorithm: "sha-256",
        value: certificateHashBytes,
      },
    ],
  },
);

The routine connect API returns a Channel and waits for its first ready generation. Use timeoutMs or signal to bound initial readiness, channel.waitUntilReady() before work that should wait through a later reconnect, and channel.close() during shutdown. Import RawWebTransport from trevrpc-js/advanced only for advanced single-session ownership.

All TrevRPC-controlled channel implementations keep 0-RTT application data disabled. TLS resumption is only a handshake optimization and never permits replaying or moving an RPC.

Limits and Shutdown

WebTransport uses the same TrevRPC server options as native QUIC:

Option Applies To WebTransport
Max concurrent connections WebTransport sessions or connections
Max concurrent streams per connection Bidirectional WebTransport streams per session or connection
Max concurrent requests Total active RPC handlers
Max frame size TrevRPC frame size inside WebTransport streams
Graceful shutdown timeout Drain timeout for active sessions and streams
Stream limits and idle timeout Streaming request and response enforcement

Go starts WebTransport shutdown by cancelling the ServeQUIC context. Rust starts shutdown through serve_quinn_and_webtransport_with_shutdown or serve_webtransport_with_shutdown. Kotlin calls NettyRpcServer.shutdown(), which drains the core server and closes sessions, connections, the listener, and event-loop resources.

Current Gaps

  • No gRPC-Web, Connect, or HTTP/JSON compatibility is provided by WebTransport support.
  • The WebTransport URL path is not a TrevRPC route. Service and method routing still comes from the TrevRPC RpcRequest frame.
  • Browser JavaScript cannot expose native QUIC; Node native support is available through the addon-backed trevrpc-js/node API.
  • Kotlin currently provides server-side WebTransport only; Kotlin clients use native QUIC or ordinary HTTP/3 through Netty or Cronet.
  • Browser QUIC flow control, ACK behavior, TCP_NODELAY, and send buffering are not exposed to JavaScript and cannot be normalized with native QUIC benchmark rows.