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
#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
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
$ ./wt-session --port 4433conn closed by peer (CONNECTION_CLOSE)session /chatprotocol chatconn closed by peer (CONNECTION_CLOSE)session closed$ go run . https://127.0.0.1:4433/forbidden: 405/chat: 200 protocol "chat"closed /chatHow it works
Section titled “How it works”wt_resource_checkruns before a session exists. Leaveoutuntouched to accept, or setout->status(here405) to send that status instead. Rejected requests never reachwt_on_session.opt.wt_protocolsis the space-separated list of subprotocols the server supports. The one the client also offered arrives asprotocolinwt_on_session.wt_on_session_closefires exactly once per session, however it ended: the client closed it, the server closed it withwired_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 peerline is the rejected/forbiddenconnection going away. - The client ends the
/chatsession by closing its QUIC connection, so the server logs the connection close first and thensession closed.