Changelog
View Source0.5.9 - 2026-11-15
- Requires quic 3.0.0 and h2 0.18.0. Over HTTP/3, a peer certificate is now refused when its Extended Key Usage or Key Usage does not allow TLS server or client authentication. The largest datagram a session reports, and accepts, is now what fits the path as confirmed so far, so it can start smaller and grow during the connection; a larger datagram is refused instead of being sent and lost.
0.5.8 - 2026-11-13
- Over HTTP/2, a client session that ended (for example after a drain)
could kill the process that called
connectwith{shutdown, {owner_exit, normal}}. That process is no longer linked to the HTTP/2 connection, only to the session. - Incoming datagrams are dropped once a session has 1024 messages waiting, instead of queueing without limit when the application falls behind (draft-ietf-webtrans-overview-14 §4.2).
- Over HTTP/3, a connection now remembers at most 256 ended sessions, to reset their late streams. Before, every ended session was kept until the connection closed, so a client opening and closing sessions on one connection grew it without limit.
- Data waiting for the peer's flow-control credit is now capped per session
by the new
max_send_bufferoption (4 MiB by default). Once it is full, a send that cannot go out at once returns{error, send_buffer_full}. Before, a peer that never granted credit made the queue grow without limit, and each credit update copied the whole queue. - A client
cacertfilethat could not be read, or held no certificate, was skipped, and over HTTP/3 the server was then checked against the system trust store instead of the CAs you chose.connectnow fails with{error, {cacertfile, File, Reason}}. Over HTTP/3 an unusable clientcertfileorkeyfilefails the same way.
0.5.7 - 2026-11-13
- Over HTTP/3, a session now ends when its connection closes, as draft-ietf-webtrans-http3-17 §6 requires. Before, server and client sessions kept running after their connection was gone.
- The HTTP/3 listener now stops its per-connection state when a connection closes. Before, it was kept for connections that never opened a WebTransport stream, and memory grew with every such connection.
- A peer can no longer make a session hold a large unfinished capsule on the CONNECT stream. Padding and unknown capsules are skipped as they arrive (RFC 9297 §3.2), oversized datagrams are discarded (RFC 9297 §3.5), and over HTTP/2 a stream capsule larger than the flow-control window ends the session with a flow-control error (draft-ietf-webtrans-http2-16 §6.6). Before, each session could hold up to 64 MiB.
- An open stream no longer keeps a copy of everything received on it. The data was already handed to the handler, and memory grew for as long as the stream stayed open.
- Breaking:
webtransport_stream:recv_buffer/1andwebtransport_stream:flush_recv_buffer/1are removed. They returned that copy, which the library never read. - Structured Field numbers (WebTransport-Init, WT-Available-Protocols, WT-Protocol) are refused as soon as they pass the length RFC 9651 §4.2.4 allows, instead of after the whole number was read.
0.5.6 - 2026-11-13
- Requires quic 2.7.0 and h2 0.17.0. Over HTTP/3, a client now rejects a server certificate whose names do not match the host under RFC 9525 (partial wildcards, certificates matched only by common name, and IP addresses not listed in the certificate). quic now pulls in idna.
0.5.5 - 2026-11-10
- Requires h2 0.15.1. Over HTTP/2, a session now learns at once that the peer closed the connection, instead of about five seconds later.
0.5.4 - 2026-11-10
- New client option
socket_opts: a list of gen_tcp/inet options for the client socket, such as the source address to bind or buffer sizes. Before, socket options passed toconnectwere lost. On HTTP/3 only the options that apply to UDP are used and TCP-only ones are ignored; on HTTP/2 they are set on the TCP socket. Without the option nothing changes.
0.5.3 - 2026-11-08
- Requires quic 2.6.1.
- When
open_streamfails because the QUIC connection is out of streams, the handler is now told throughhandle_stream_credit/2when it can try again, as it already was for the session's own stream limit. Before, you had to poll. This matters most for Chrome, Firefox and Safari 26 sessions, where the QUIC limit is the only one. - A client whose server does not allow a new stream yet now waits for one,
within
timeout, instead of failing the connect at once. This applies to HTTP/3 and HTTP/2.
0.5.2 - 2026-11-08
- Accepts h2 0.15.0. 0.5.1 required h2 0.14, so a project that also needs h2 0.15, such as hackney, could not resolve its dependencies. Nothing else changes.
0.5.1 - 2026-11-08
- Safari 26 can now open WebTransport sessions over HTTP/3. It speaks
draft-07 and gave up on servers that did not advertise it, so it failed
against 0.5.0. Listeners in the default
automode and inlegacy_browser_compatnow advertise draft-07 and accept one draft-07 session per connection. The 0.5.0 notes were wrong to say Safari 26 uses draft-15. Clients can speak draft-07 withcompat_mode => draft07. - Flow control on HTTP/3 now depends only on whether both sides announced
limits, whatever the draft. Safari 26 announces them and waits for more
credit. Clients in
legacy_browser_compatmode announce none, and no longer stall after the server's first megabyte waiting for credit the server never sends. Theirsession_info/1now reportsdraft02instead ofh3_17. - A single stream write of more than 1 MiB over HTTP/2 no longer fails. When the peer granted large limits (Safari grants 8 MiB), the HTTP/2 layer refused the write and the data was lost. The write now waits for the peer to accept it.
- Resetting a stream you opened now works with draft-02 and draft-07 peers. The reset was silently not sent when the peer did not support partial resets, which those drafts predate.
0.5.0 - 2026-11-08
This release moves to the current WebTransport drafts: HTTP/3 draft-17, HTTP/2 draft-16 and the protocol framework draft-14. It changes what goes on the wire, so peers on older drafts may no longer connect.
HTTP/3 servers and clients on draft-17 now find each other. The server still accepts draft-15 clients (such as Safari 26). Draft-02 clients (Chrome, Firefox) keep working through
compat_mode, unchanged.HTTP/2 now follows draft-16. HTTP/2 peers still on draft-14 no longer interoperate: the end-of-stream marker on stream data, the session close message and stream resets all changed in the draft.
Sessions can negotiate a subprotocol. Clients pass
protocols(and optionallyrequire_protocol), servers pick one with the newselect_protocol/2handler callback, and the choice shows up in the request map and insession_info/1.Flow control now follows the drafts on both transports. Limits come from the peer's settings, stream limits count every stream opened in the session, and credit is given back as streams finish, so long-lived sessions no longer run out of streams. A peer that sends more data or opens more streams than it was allowed, or lowers a limit, now has its session closed instead of having the excess dropped. HTTP/3 sends are now held back when the peer's session limit is reached.
WebTransport over HTTP/2 now requires TLS 1.3, as the draft asks for TLS 1.3 or a TLS 1.2 feature OTP does not provide. The HTTP/2 listener offers only TLS 1.3, clients refuse to open a session over anything else, and
accept/4resets such a request. A server that later turns WebTransport support into an invalid value now closes the client's connection, and session limits use the settings in effect when the session was set up.accept/4now works from an HTTP/2 server's request handler. h2 passes the method and path as arguments, whichaccept/4did not see, so it always answered{error, not_connect_method}. Pass the path with the newpathoption. Sessions on the HTTP/2 listener also report their real path instead of/.New
max_stream_dataoption sets the per-stream window granted on HTTP/2. The HTTP/2 WebTransport-Init header now carries per-stream limits, as the draft defines it, and a malformed one is refused with 400.A handler that failed to open a stream because of the peer's limit is told when it can try again, through the new
handle_stream_credit/2callback; the default client handler forwards it as astream_creditmessage.On HTTP/2, invalid stream operations by the peer (data or a reset on a finished stream, a second stop-sending request) close the session, and a stop-sending request is answered with a reset, as the draft requires. Resetting a stream now ends only our sending side; the peer can still finish sending on a bidirectional stream.
Closing a session from the server now reaches the client with its code and reason on both transports; before, an HTTP/3 client never saw it. The reason must be valid UTF-8:
close_session/3returns{error, invalid_reason}or{error, invalid_error_code}otherwise.A session also ends when the peer closes or resets its session stream without a close message, and a malformed session message ends it instead of crashing the session process. Draining sessions no longer crash on incoming messages.
On HTTP/3 the connection is closed only when its last session ends, and a client's HTTP/2 connection is closed with its session instead of lingering.
Streams and datagrams that arrive just before their session is set up are held and delivered instead of being reset or dropped.
Error codes of a stream reset from an HTTP/3 peer that are not application codes are now reported as
undefinedinstead of a raw transport code, and a reset with code 0 is no longer mistaken for a clean end of stream.A session always tells the peer when it ends, including when the handler stops, crashes or a drain completes; before, the peer was left waiting.
Actions returned by a handler's
init/3now run once the session is set up, after the server's response. Before, they could go out ahead of it, and opening a stream frominit/3failed on HTTP/3.Draining sessions now stop once their streams are done, can still close or reset those streams, and keep exchanging datagrams. An HTTP/3 GOAWAY now drains the sessions on the connection.
On HTTP/2, a reset or stop-sending request for a stream the peer has not used yet now opens it as the draft requires, so two instances of this library no longer end each other's sessions.
close_stream/2after a large send now waits for the queued data before ending the stream, instead of cutting it short.Finished streams are released, so long sessions no longer grow in memory.
On HTTP/3, data the peer sent on a stream before resetting it now counts against the session's data limit, so many resets no longer leave a peer stalled. When the peer stops reading a unidirectional stream we opened, the handler is told and further sends on it fail at once.
session_info/1reportsmax_datagram_size, the largest datagram the application can send.A server no longer closes the HTTP/3 connection when its last WebTransport session ends, so other requests on it are not cut off. A client closes its connection once the server has closed the session.
reset_streamandstop_sendingrefuse error codes above 32 bits with{error, invalid_error_code}.quic bumped to 2.5.0. Since 2.2.1 it fixes a header decoding issue that let an HTTP/3 peer burn minutes of CPU with one request (GHSA-224p-pchv-5r3r), a listener option that claimed to require client certificates but accepted every client (GHSA-wc8g-r527-q23h; this library does not set it), and a bug that reset a WebTransport client's session stream at the first message the server sent on it. It also adds what WebTransport draft-17 needs: requests wait for the client's settings, connections can be closed with an HTTP/3 error code, a stream reset reports its final size, and our streams keep their header when quic resets them on its own. The version constraint is now
~> 2.5.Erlang/OTP 27 or later is now required, as quic 2.4 and later no longer support OTP 26.
h2 bumped to 0.14.0. It fixes two denial-of-service issues, one where a client could run more requests at once than the server allows (MadeYouReset, CVE-2025-8671) and a SETTINGS flood (CVE-2019-9515), and adds what WebTransport over HTTP/2 needs: the TLS version of a connection, notice of SETTINGS changes, and the SETTINGS in effect when a request or response is sent. It also stops an HTTP/2 server from refusing connections once the process that started it exits, and makes stopping a server immediate. The version constraint is now
~> 0.14.0.
0.4.8 - 2026-10-03
- Payloads larger than one HTTP/2 DATA frame are no longer truncated on h2.
A WT_STREAM capsule carries a whole
send/4payload and h2 splits it into 16 KiB frames, but the CONNECT-stream reader decoded each frame on its own and dropped the partial tail. The reader now carries the tail over to the next frame. A single capsule over 64 MiB closes the session. - Sends larger than the per-stream window no longer lose data. The stream
cut the payload at its window (256 KiB by default), kept the rest in a
buffer nothing drained, and still sent FIN. On h3 the WebTransport window
is now unbounded, since QUIC does the flow control there. On h2 the bytes
that do not fit are queued in order, FIN waits until the queue drains, and
the queue is flushed when the peer raises
WT_MAX_DATAorWT_MAX_STREAM_DATA. - The h2 receiver now returns flow-control credit: once half of a stream or
session window is consumed it sends
WT_MAX_STREAM_DATAorWT_MAX_DATA. Before, a peer that honoured our windows stalled for good after 256 KiB on a stream or 1 MiB on a session. - New tests: a 1 MiB bidi echo on both transports, and unit tests for the frame reassembly.
- The per-connection router reaper no longer crashes with
badargwhen thewebtransport_routerstable is already gone, which happens when an embedder stops its listener while connections are still closing. - Closing a session whose transport is already gone no longer crashes the
session with
noproc. The stream resets and the CLOSE_SESSION send are skipped when the connection process has exited, soclose_session/1always completes.
0.4.7 - 2026-09-24
- Bump
quicdep to 2.0.0, constraint~> 2.0. - Lock
h2to 0.12.3 (constraint unchanged,~> 0.12). - A connection that closes while a client waits for its CONNECT response is reported again. quic 2.0.0 always sends the close reason with the event, and the receive only matched the older shape without one, so the close was not seen and the caller waited out its timeout. Both shapes are matched.
0.4.6 - 2026-09-21
- Bump
quicdep to 1.10.0. The constraint is now~> 1.10(1.10 and up within 1.x) instead of a patch-pinned~> 1.8.0, so a project can take newer quic releases without a webtransport release. Picks up the RFC 9002 loss detection and recovery rework, connection ID binding to paths, and the 1.9 fixes for multi-datagram Initial flights and frames that waited for the next event after the handshake. No source changes needed.
0.4.5 - 2026-08-12
- Bump
h2dep to 0.12.0. The constraint is now~> 0.12(0.12 and up within 0.x) instead of a patch-pinned~> 0.12.0. Picks uph2:serve_socket/2for embedders owning their own TLS listener, and moves the TLS handshake into the per-connection process so a stalled client no longer blocks the acceptor queue.start_server/2behaviour is unchanged; no source changes needed.
0.4.4 - 2026-08-06
- Bump
quicdep to 1.8.0 (~> 1.8.0, locked at 1.8.0). Picks up RFC 6125 wildcard SAN matching for client hostname checks, Happy Eyeballs backlog delivery on the winning attempt, Retry packet-number continuity, Version Negotiation handling, and amax_udp_payload_sizethat reflects what we accept rather than the probing ceiling. - Bump
h2dep to 0.11.0 (~> 0.11.0, locked at 0.11.0). Addsh2:peername/1.
0.4.3 - 2026-07-06
- Bump
quicdep to 1.7.0 (~> 1.7.0, locked at 1.7.0).
0.4.2 - 2026-07-05
- Bump
h2dep to 0.10.4 (~> 0.10.4, locked at 0.10.4).
0.4.1 - 2026-06-14
- Bump
h2dep to 0.10.1. - Accept patch releases of deps via
~>constraints:h2 ~> 0.10.1,quic ~> 1.6.5(locked at 0.10.1 / 1.6.5).
0.4.0 - 2026-06-12
- Bump
quicdep to 1.6.5. - Per-SNI certificate selection. Listeners accept an
sni_callbackoption (fun((ServerName) -> {ok, #{cert, key, cert_chain}} | {error, _})) that picks the server certificate per ClientHello SNI (RFC 6066 §3), so one listener can present different certs per hostname. For h3 the callback is forwarded toquic; for h2 it is adapted to ansslsni_fun. A callback error fails the handshake on both transports. When a client sends no SNI, h2 serves the staticcertfile/keyfile(ssl never calls the callback) while h3 invokes the callback withundefined. - Test: SNI round-trip on both h3 and h2 groups.
0.3.3 - 2026-06-10
- Bump
h2dep to 0.9.0.
0.3.2 - 2026-06-05
- Bump
quicdep to 1.6.4.
0.3.1
- Bump
quicdep to 1.6.3.
0.3.0
- Bump
quicdep to 1.6.2,h2dep to 0.8.0. - IPv6: listeners bind via
ip/family(h3 and h2); clients accept IP-literal hosts andinet:ip_address()tuples, withfamily/happy_eyeballs/connection_attempt_delaycontrols (h3). IPv6 literals are bracketed in the:authorityheader. webtransport:listener_sockname/1returns the bound{Ip, Port}(also surfaced assocknameinlistener_info/1). Resolved from the socket for h3; best-effort (requested addr + bound port) for h2, which exposes only the port.- 0-RTT: session-ticket capture (
{webtransport, session_ticket, _}to the connecting process) and connection-level acceptance reporting viawebtransport:early_data_accepted/1. Thesession_ticketconnect option plumbs a stored ticket through to QUIC. Full 0-RTT resumption is not yet supported through the synchronous connect path. - Tests: IPv6 round-trip (h3 + h2), sockname, and session-ticket capture.
0.2.6
- Fix: failed h3
connect/4no longer leaks its per-connection router. The router isstart_link'd to the caller but traps exits, so the caller link never reaped it;connect_h3now stops it on the connect-failure path, and closes the H3 connection (which reaps the router via its monitor) when the session fails to start. - Fix: the h2 CONNECT-stream data loop now monitors both the h2 connection and the session and exits on either
'DOWN', instead of blocking forever if the connection dies without aclosedmessage or the session goes away. Server and client now spawn it the same way. - Bound the router
open_bidi_streamcall (wasinfinity) so an unresponsive H3 connection cannot stall the session process. - Tests: regressions for the router leak and the data-loop termination.
0.2.5
- Bump
quicdep to 1.4.5,h2dep to 0.6.1. - OTP 29 support: replace deprecated
catch Exprexpressions withtry ... catch ... end(webtransport_h2,webtransport_session,webtransport). - CI: add OTP 28 and 29 to the test matrix; bump rebar3 to 3.25.0.
0.2.4
- Bump
quicdep to 1.4.2.
0.2.3
- Embedding (h3):
accept/4now works when called from a process other than the QUIC connection process. The per-connection stream router is keyed by the QUIC connection pid, shared by theh3_settings/1connection_handler and byaccept/4(viaquic_h3:get_quic_conn/1), instead of the caller's process dictionary. Incoming bidi/uni streams and datagrams now reach the session even when the embedder dispatches each request to its own worker (e.g. Livery). The router's registry row and process are reaped when its QUIC connection ends. - CT:
embedded_accept_round_trip_test(h3) drives a rawquic_h3server withh3_settings/0merged in, accepts from a worker process, and asserts that a bidi stream (with FIN) and a datagram both round-trip through the echo handler.
0.2.2
- Docs: README gains a Sponsors section (Enki Multimedia logo vendored under
docs/images/). Docs: fix invalid
webtransport:open_stream(Session, bidi | uni)snippet — type-union pipe was in expression position; nowbidi %% or uni.- CT: cover
listeners/0,listener_info/1, andstart_listener/2cert/key error paths (webtransport.erlline coverage 51% → 54%).
0.2.1
start_listener/2(h3): switch the listener loop fromspawn_link/3tospawn/3sostop_listener/1no longer propagates ashutdownexit to the caller. The interactive shell (and any non-trapping caller) now stays clean across a full lifecycle.webtransport_session:init/1: distinguish a handler module that fails to load ({handler_not_loaded, M, Reason}) from a loaded module missinginit/2orinit/3.- README: quick-start now launches the shell with
ERL_FLAGS="-pa examples" rebar3 shell --apps webtransportso handler modules outsidesrc/are reachable. - Regression test:
stop_listener_does_not_kill_caller_test(h3 + h2 groups).
Known issue (tracked upstream in quic): a graceful close_session/1 still emits a quic_h3_connection quic_closed CRASH REPORT because the dep treats any QUIC-conn 'DOWN' as abnormal. Will resolve when quic is bumped.
0.2.0
- Switch
h2andquicdeps to hex packages:h2 0.5.0,quic 1.3.0. - Add
public_keyto the application list. - Clean dialyzer run: dropped dead
{error, _}clauses aroundh2_capsule:decode/1andh2_varint:decode/1, removed unreachableRouter =:= undefinedbranch, dropped unsupportedh3_datagram_enabledfromquic_h3:start_server/3opts, switched listener loop tospawn_link/3for static reachability. - Configure
xref_checks(skipexports_not_usedfor the public API) anddialyzer(plt_extra_apps: [public_key, ssl, crypto, inets]). - Add GitHub Actions CI: OTP 26/27 matrix runs compile, xref, eunit, dialyzer; separate job builds ex_doc.
0.1.0
- Initial release.