Configuration
View SourceServer Options
webtransport:start_listener(Name, Opts).| Option | Required | Default | Description |
|---|---|---|---|
transport | yes | -- | h2 (HTTP/2) or h3 (HTTP/3) |
port | yes | -- | TCP/UDP port to listen on |
certfile | yes | -- | Path to TLS certificate (PEM) |
keyfile | yes | -- | Path to TLS private key (PEM) |
handler | yes | -- | Module implementing webtransport_handler |
handler_opts | no | #{} | Map passed to handler:init/3 |
max_data | no | 1048576 (1 MB) | Session-level flow-control window (bytes) |
max_streams_bidi | no | 100 | Max concurrent bidirectional streams |
max_streams_uni | no | 100 | Max concurrent unidirectional streams |
compat_mode | no | auto | HTTP/3 draft selection (see below) |
Client Options
webtransport:connect(Host, Port, Path, Opts).| Option | Default | Description |
|---|---|---|
transport | h3 | h2 or h3 |
verify | verify_peer | verify_peer or verify_none |
cacertfile | -- | Path to CA certificate bundle |
certfile | -- | Client certificate (mutual TLS) |
keyfile | -- | Client private key (mutual TLS) |
headers | [] | Extra headers on the CONNECT request |
timeout | 30000 | Connection timeout (ms) |
handler_opts | #{} | Map passed to handler's init/3 |
compat_mode | latest | HTTP/3 draft selection (see below) |
Compatibility Mode
The HTTP/3 WebTransport spec has evolved through multiple drafts. As of April 2026, Safari and the IETF are on draft-15 while Chrome and Firefox still use draft-02. This library keeps the two paths separate:
| Mode | :protocol | SETTINGS | Use when |
|---|---|---|---|
latest | webtransport-h3 | wt_enabled=1 + initial flow-control | Draft-15 peers (Safari, spec-conformant servers) |
legacy_browser_compat | webtransport | SETTINGS_ENABLE_WEBTRANSPORT_DRAFT02=1 | Draft-02 peers (Chrome, Firefox, quic-go v0.9) |
auto (server only) | accepts both | advertises both | Accept either draft per request |
Server detection
In auto mode, the server inspects each CONNECT request:
:protocol = webtransport-h3with no draft-02 header -> latest:protocol = webtransport-> legacy_browser_compat- Conflicting signals (e.g.
webtransport-h3plus the draft-02 header) -> 400
The decision is frozen at session init. Pin to latest or
legacy_browser_compat to refuse the other:
%% Accept only draft-15 clients
webtransport:start_listener(strict, #{
transport => h3,
port => 4433,
certfile => "cert.pem",
keyfile => "key.pem",
handler => my_handler,
compat_mode => latest
}).Client selection
Clients must choose explicitly. Default is latest:
{ok, Session} = webtransport:connect("example.com", 443, <<"/wt">>, #{
transport => h3,
compat_mode => legacy_browser_compat
}).HTTP/2 has no draft-02 variant; compat_mode applies only to HTTP/3.
Flow Control
WebTransport provides session-level and per-stream flow control:
| Parameter | Default | Description |
|---|---|---|
max_data | 1048576 (1 MB) | Session-level byte limit |
max_streams_bidi | 100 | Max concurrent bidirectional streams |
max_streams_uni | 100 | Max concurrent unidirectional streams |
Override at listener or connect time:
webtransport:start_listener(my_server, #{
transport => h3,
port => 4433,
certfile => "cert.pem",
keyfile => "key.pem",
handler => my_handler,
max_data => 4194304, %% 4 MB
max_streams_bidi => 200,
max_streams_uni => 50
}).Enforcement
The library enforces these rules from the spec:
- Monotonicity -- a peer sending a decreased
WT_MAX_DATAorWT_MAX_STREAMScloses the session withWT_FLOW_CONTROL_ERROR. - Peer stream count -- streams opened beyond the advertised limit are
rejected with
WT_BUFFERED_STREAM_REJECTED. - HTTP/3 prohibition --
WT_MAX_STREAM_DATAandWT_STREAM_DATA_BLOCKEDcapsules are session errors on HTTP/3 (per-stream flow control uses native QUIC). - HTTP/2 WebTransport-Init -- the
WebTransport-Initstructured-field header (draft-14 section 4.3.2) carries initial flow-control windows. When both SETTINGS and the header are present, the greater value is used.
Datagram Limits
Datagrams are bounded by the transport:
| Transport | Max payload | Reason |
|---|---|---|
| HTTP/3 | 65527 bytes | max_datagram_frame_size (65535) minus session-id varint (up to 8 bytes) |
| HTTP/2 | 65471 bytes | HTTP/2 initial stream window (65535) minus capsule framing overhead (64 bytes) |
Sending a datagram larger than the limit returns {error, datagram_too_large}.
Error Codes
The library uses the error codes defined in the spec:
| Constant | Value | Meaning |
|---|---|---|
WT_BUFFERED_STREAM_REJECTED | 0x3994bd84 | Peer exceeded buffered stream limit |
WT_SESSION_GONE | 0x170d7b68 | Session terminated |
WT_FLOW_CONTROL_ERROR | 0x045d4487 | Flow-control violation |
WT_REQUIREMENTS_NOT_MET | 0x212c0d48 | Protocol requirements not satisfied |
Application-level error codes are mapped to/from QUIC error codes per draft-15 section 3.3.
Session Termination
When a session closes (locally or by the peer):
- All live streams are reset with
WT_SESSION_GONE. - A
CLOSE_SESSIONcapsule is sent (or received) with an error code and reason (max 1024 bytes). - The CONNECT stream is half-closed (FIN sent).
- The handler's
terminate/2receives{closed, ErrorCode, Reason}.
If the peer FINs the CONNECT stream without sending CLOSE_SESSION, the
session terminates with {closed, 0, <<"peer closed CONNECT">>}.