Skip to content

WebTransport sessions

A session starts with an Extended CONNECT request. The server can reject the :path, pick a subprotocol, and learn when the session ends.

Code

main.c
#define WIRED_MAIN
#include "wired.h"
/* Decide per :path, before the session exists. Leaving `out` untouched
* accepts; a non-zero status is sent instead of 200. */
static void on_resource(
void* ctx,
wired_span authority,
wired_span path,
wired_wt_resource_decision* out) {
(void)ctx;
(void)authority;
if (wired_span_eq_cstr(path, "/forbidden")) out->status = 405;
}
static void on_session(
void* ctx, wired_wt_session* s, wired_span path, wired_span protocol) {
(void)ctx;
(void)s;
wired_dprintf(2, "session %.*s\n", WIRED_SPAN_ARG(path));
wired_dprintf(2, "protocol %.*s\n", WIRED_SPAN_ARG(protocol));
}
/* Fires once per session, however it ended. Release per-session state here. */
static void on_session_close(void* ctx, wired_wt_session* s) {
(void)ctx;
(void)s;
wired_log_str("session closed\n");
}
int wired_main(int argc, char** argv) {
/* The same fixed demo identity as the HTTP/3 hello page. */
static wired_srvboot_demo_keys keys;
wired_srvboot_id id;
wired_srvboot_demo(&id, &keys, 0x50, "guide-wt");
id.max_datagram_frame_size = 65535; /* WebTransport requires DATAGRAM */
wired_srvrun_opt opt = {0};
opt.incoming_cpu = -1;
opt.wt_resource_check = on_resource;
opt.wt_protocols = "chat"; /* space-separated list we accept */
opt.wt_on_session = on_session;
opt.wt_on_session_close = on_session_close;
u16 port = (u16)wired_cliargs_int(argc, argv, "--port", 4433);
wired_srvrun_handler h = {0};
wired_srvrun_obs obs = {0};
return wired_server_run_opt(port, &id, h, obs, &opt) ? 0 : 1;
}

Client

client.go
package main
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"github.com/quic-go/quic-go"
"github.com/quic-go/webtransport-go"
)
func main() {
ctx := context.Background()
var conn *quic.Conn // the latest connection, kept so main can close it
d := webtransport.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // self-signed demo cert
QUICConfig: &quic.Config{EnableDatagrams: true, EnableStreamResetPartialDelivery: true},
ApplicationProtocols: []string{"chat"},
DialAddr: func(ctx context.Context, addr string, t *tls.Config, q *quic.Config) (*quic.Conn, error) {
c, err := quic.DialAddrEarly(ctx, addr, t, q)
conn = c
return c, err
},
}
rsp, _, err := d.Dial(ctx, os.Args[1]+"/forbidden", http.Header{})
if err == nil {
panic("/forbidden was accepted")
}
fmt.Println("/forbidden:", rsp.StatusCode)
rsp, s, err := d.Dial(ctx, os.Args[1]+"/chat", http.Header{})
if err != nil {
panic(err)
}
fmt.Printf("/chat: %d protocol %q\n", rsp.StatusCode, s.SessionState().ApplicationProtocol)
conn.CloseWithError(0, "") // ending the connection ends its session too
fmt.Println("closed /chat")
}

Output

Terminal window
$ ./wt-session --port 4433
conn closed by peer (CONNECTION_CLOSE)
session /chat
protocol chat
conn closed by peer (CONNECTION_CLOSE)
session closed
Terminal window
$ go run . https://127.0.0.1:4433
/forbidden: 405
/chat: 200 protocol "chat"
closed /chat
  • wt_resource_check runs before a session exists. Leave out untouched to accept, or set out->status (here 405) to send that status instead. Rejected requests never reach wt_on_session.
  • opt.wt_protocols is the space-separated list of subprotocols the server supports. The one the client also offered arrives as protocol in wt_on_session.
  • wt_on_session_close fires exactly once per session, however it ended: the client closed it, the server closed it with wired_server_wt_close_session, or the connection went away. Free any state you keyed on the session pointer here, because the slot is reused.
  • The first conn closed by peer line is the rejected /forbidden connection going away.
  • The client ends the /chat session by closing its QUIC connection, so the server logs the connection close first and then session closed.