Connection Upgrades
WebSocket and WebTransport over HTTP tunnels.
HTTP tunnels support connection upgrades. This covers the two most common upgrade protocols: WebSocket and WebTransport.
WebSocket
rstream supports WebSocket on both the downstream (client-to-engine) and upstream (engine-to-server) legs across all HTTP versions. The downstream and upstream versions are independent: the engine performs protocol translation automatically.
Supported combinations
| Downstream (client → engine) | Upstream (engine → server) | Mechanism |
|---|---|---|
| HTTP/1.1 | h1 | RFC 6455 upgrade |
| HTTP/1.1 | h2c | RFC 6455 → RFC 8441 translation |
| HTTP/1.1 | h3 | RFC 6455 → RFC 9220 translation |
| HTTP/2 | h1 | RFC 8441 → RFC 6455 translation |
| HTTP/2 | h2c | RFC 8441 Extended CONNECT |
| HTTP/2 | h3 | RFC 8441 → RFC 9220 translation |
| HTTP/3 | h1 | RFC 9220 → RFC 6455 translation |
| HTTP/3 | h2c | RFC 9220 → RFC 8441 translation |
| HTTP/3 | h3 | RFC 9220 Extended CONNECT |
The upstream HTTPVersion in TunnelProperties controls which protocol the engine uses toward the upstream server. The downstream version is independent — any HTTP client can connect regardless of the upstream setting.
HTTP/1.1 upstream
The standard WebSocket upgrade path. Any library that works over net.Listener is compatible on the server side.
tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
Name: rstream.StringPtr("ws-server"),
Publish: rstream.BoolPtr(true),
Protocol: rstream.ProtocolPtr(rstream.ProtocolHTTP),
HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP1_1),
})
if err != nil {
log.Fatal(err)
}
defer tunnel.Close()
listener, ok := tunnel.(net.Listener)
if !ok {
log.Fatal("tunnel does not implement net.Listener")
}
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
// upgrade and handle the connection
})
log.Fatal(http.Serve(listener, nil))HTTP/2 cleartext upstream (RFC 8441)
Setting HTTPVersion to HTTP2 instructs the engine to reach the upstream server over HTTP/2 cleartext using Extended CONNECT. The upstream server receives a CONNECT request with :protocol: websocket and handles WebSocket frames over that stream.
tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
Name: rstream.StringPtr("ws-server"),
Publish: rstream.BoolPtr(true),
Protocol: rstream.ProtocolPtr(rstream.ProtocolHTTP),
HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP2),
})HTTP/3 upstream (RFC 9220)
Setting HTTPVersion to HTTP3 instructs the engine to reach the upstream server over HTTP/3 using Extended CONNECT. The tunnel type must be TunnelTypeDatagram because HTTP/3 runs over QUIC.
tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
Name: rstream.StringPtr("ws-server"),
Type: rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
Publish: rstream.BoolPtr(true),
Protocol: rstream.ProtocolPtr(rstream.ProtocolHTTP),
HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
})Server-side considerations
For an H1 upstream, any standard WebSocket library works without modification — the engine forwards the standard Upgrade: websocket handshake unchanged.
For H2C and H3 upstreams, the upstream server receives WebSocket over Extended CONNECT. The server must handle a CONNECT request carrying :protocol: websocket (RFC 8441 for H2C, RFC 9220 for H3). Libraries that only implement RFC 6455 are not compatible with this handshake form; the upstream must either use an HTTP-version-agnostic library or handle the CONNECT method directly. The http-ws-h2c-server and http-ws-h3-server examples provide minimal reference implementations.
The downstream client is not affected by the upstream setting. Any WebSocket client connects using its native protocol; the engine translates to the configured upstream version transparently.
Token authentication, rstream Auth, and challenge mode apply to the initial WebSocket upgrade request.
WebTransport
WebTransport is a browser API that provides multiplexed bidirectional streams and unreliable datagrams over an HTTP/3 session. rstream exposes WebTransport through a published HTTP/3 tunnel.
How it works
An HTTP/3 tunnel creates a datagram tunnel endpoint published on the edge network. The engine accepts WebTransport sessions from downstream clients using the Extended CONNECT mechanism (:protocol: webtransport), establishes a matching session to the upstream service over the tunnel, and relays bidirectional streams, unidirectional streams, and datagrams between the two sessions end-to-end.
Because WebTransport is carried over an HTTP/3 tunnel, all HTTP-level features are available: token authentication, rstream auth, and access policies apply to the initial session establishment. This is the key advantage of WebTransport over a raw QUIC tunnel, which has no HTTP-level authentication.
Server setup
import (
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/webtransport-go"
"github.com/rstreamlabs/rstream-go"
)
tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
Name: rstream.StringPtr("wt-server"),
Type: rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
Publish: rstream.BoolPtr(true),
Protocol: rstream.ProtocolPtr(rstream.ProtocolHTTP),
HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
})
if err != nil {
log.Fatal(err)
}
defer tunnel.Close()
forwardingAddr, err := tunnel.ForwardingAddress()
if err != nil {
log.Fatal(err)
}
fmt.Println("WebTransport endpoint:", forwardingAddr)
packetListener, ok := tunnel.(rstream.PacketListener)
if !ok {
log.Fatal("tunnel does not implement rstream.PacketListener")
}
mux := http.NewServeMux()
wtServer := webtransport.Server{
H3: &http3.Server{
Handler: mux,
TLSConfig: yourTLSConfig,
EnableDatagrams: true,
},
}
webtransport.ConfigureHTTP3Server(wtServer.H3)
mux.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
sess, err := wtServer.Upgrade(w, r)
if err != nil {
http.Error(w, "upgrade failed", http.StatusBadRequest)
return
}
go handleSession(sess)
})
log.Fatal(wtServer.Serve(rstream.PacketConnFromPacketListener(packetListener)))Session handling
A WebTransport session exposes bidirectional streams, unidirectional streams, and datagrams. All four directions are relayed through the engine independently.
func handleSession(sess *webtransport.Session) {
ctx := sess.Context()
// Accept a client-opened bidirectional stream
stream, err := sess.AcceptStream(ctx)
if err != nil {
return
}
defer stream.Close()
io.Copy(stream, stream) // echo
// Open a server-initiated bidirectional stream
out, err := sess.OpenStreamSync(ctx)
if err != nil {
return
}
out.Write([]byte("hello from server"))
out.Close()
// Accept a client-opened unidirectional stream
rs, err := sess.AcceptUniStream(ctx)
if err != nil {
return
}
io.ReadAll(rs)
// Send and receive datagrams
sess.SendDatagram([]byte("ping"))
payload, err := sess.ReceiveDatagram(ctx)
// Close the session with an application code and reason
sess.CloseWithError(webtransport.SessionErrorCode(0), "done")
}Application-level close codes
CloseWithError takes an error code and a reason string. Both propagate through the relay to the other side. Downstream clients receive the exact code and reason set by the upstream application, and vice versa. This lets applications signal termination intent without relying on stream-level errors.
Token authentication
When a tunnel is created with TokenAuth: true, the engine validates the downstream client's token before upgrading the WebTransport session. The token is supplied by the client in the Authorization: Bearer header or the rstream.token query parameter, identical to any other HTTP tunnel request.
tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
Name: rstream.StringPtr("wt-server"),
Type: rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
Publish: rstream.BoolPtr(true),
Protocol: rstream.ProtocolPtr(rstream.ProtocolHTTP),
HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
TokenAuth: rstream.BoolPtr(true),
})A WebTransport client connecting to a token-protected tunnel supplies the token in the initial HTTP/3 CONNECT request:
// Browser — add the token as a query parameter
const transport = new WebTransport(
"https://endpoint.example.rstream.test/app?rstream.token=<token>"
);
await transport.ready;WebTransport vs raw QUIC tunnels
Both WebTransport tunnels (Protocol: HTTP, HTTPVersion: h3) and raw QUIC tunnels (Protocol: QUIC) carry QUIC-based traffic, but they serve different needs.
| WebTransport tunnel | QUIC tunnel | |
|---|---|---|
| Protocol | HTTP/3 + Extended CONNECT | Raw QUIC |
| Authentication | Token auth, rstream auth, access policies | Transport-level mTLS only |
| Streams | Multiplexed by session path | Per-connection |
| Datagrams | Yes | Yes |
| Browser client | Yes (W3C WebTransport API) | No |
Use a WebTransport tunnel when the client is a browser or when HTTP-level authentication and access policies matter. Use a raw QUIC tunnel for custom protocols that speak QUIC natively and do not need HTTP framing.