Changelog

View Source

0.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 connect with {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_buffer option (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 cacertfile that 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. connect now fails with {error, {cacertfile, File, Reason}}. Over HTTP/3 an unusable client certfile or keyfile fails 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/1 and webtransport_stream:flush_recv_buffer/1 are 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 to connect were 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_stream fails because the QUIC connection is out of streams, the handler is now told through handle_stream_credit/2 when 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 auto mode and in legacy_browser_compat now 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 with compat_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_compat mode announce none, and no longer stall after the server's first megabyte waiting for credit the server never sends. Their session_info/1 now reports draft02 instead of h3_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 optionally require_protocol), servers pick one with the new select_protocol/2 handler callback, and the choice shows up in the request map and in session_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/4 resets 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/4 now works from an HTTP/2 server's request handler. h2 passes the method and path as arguments, which accept/4 did not see, so it always answered {error, not_connect_method}. Pass the path with the new path option. Sessions on the HTTP/2 listener also report their real path instead of /.

  • New max_stream_data option 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/2 callback; the default client handler forwards it as a stream_credit message.

  • 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/3 returns {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 undefined instead 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/3 now run once the session is set up, after the server's response. Before, they could go out ahead of it, and opening a stream from init/3 failed 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/2 after 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/1 reports max_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_stream and stop_sending refuse 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/4 payload 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_DATA or WT_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_DATA or WT_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 badarg when the webtransport_routers table 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, so close_session/1 always completes.

0.4.7 - 2026-09-24

  • Bump quic dep to 2.0.0, constraint ~> 2.0.
  • Lock h2 to 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 quic dep 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 h2 dep 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 up h2:serve_socket/2 for 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/2 behaviour is unchanged; no source changes needed.

0.4.4 - 2026-08-06

  • Bump quic dep 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 a max_udp_payload_size that reflects what we accept rather than the probing ceiling.
  • Bump h2 dep to 0.11.0 (~> 0.11.0, locked at 0.11.0). Adds h2:peername/1.

0.4.3 - 2026-07-06

  • Bump quic dep to 1.7.0 (~> 1.7.0, locked at 1.7.0).

0.4.2 - 2026-07-05

  • Bump h2 dep to 0.10.4 (~> 0.10.4, locked at 0.10.4).

0.4.1 - 2026-06-14

  • Bump h2 dep 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 quic dep to 1.6.5.
  • Per-SNI certificate selection. Listeners accept an sni_callback option (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 to quic; for h2 it is adapted to an ssl sni_fun. A callback error fails the handshake on both transports. When a client sends no SNI, h2 serves the static certfile/keyfile (ssl never calls the callback) while h3 invokes the callback with undefined.
  • Test: SNI round-trip on both h3 and h2 groups.

0.3.3 - 2026-06-10

  • Bump h2 dep to 0.9.0.

0.3.2 - 2026-06-05

  • Bump quic dep to 1.6.4.

0.3.1

  • Bump quic dep to 1.6.3.

0.3.0

  • Bump quic dep to 1.6.2, h2 dep to 0.8.0.
  • IPv6: listeners bind via ip / family (h3 and h2); clients accept IP-literal hosts and inet:ip_address() tuples, with family / happy_eyeballs / connection_attempt_delay controls (h3). IPv6 literals are bracketed in the :authority header.
  • webtransport:listener_sockname/1 returns the bound {Ip, Port} (also surfaced as sockname in listener_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 via webtransport:early_data_accepted/1. The session_ticket connect 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/4 no longer leaks its per-connection router. The router is start_link'd to the caller but traps exits, so the caller link never reaped it; connect_h3 now 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 a closed message or the session goes away. Server and client now spawn it the same way.
  • Bound the router open_bidi_stream call (was infinity) 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 quic dep to 1.4.5, h2 dep to 0.6.1.
  • OTP 29 support: replace deprecated catch Expr expressions with try ... 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 quic dep to 1.4.2.

0.2.3

  • Embedding (h3): accept/4 now 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 the h3_settings/1 connection_handler and by accept/4 (via quic_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 raw quic_h3 server with h3_settings/0 merged 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; now bidi %% or uni.

  • CT: cover listeners/0, listener_info/1, and start_listener/2 cert/key error paths (webtransport.erl line coverage 51% → 54%).

0.2.1

  • start_listener/2 (h3): switch the listener loop from spawn_link/3 to spawn/3 so stop_listener/1 no longer propagates a shutdown exit 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 missing init/2 or init/3.
  • README: quick-start now launches the shell with ERL_FLAGS="-pa examples" rebar3 shell --apps webtransport so handler modules outside src/ 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 h2 and quic deps to hex packages: h2 0.5.0, quic 1.3.0.
  • Add public_key to the application list.
  • Clean dialyzer run: dropped dead {error, _} clauses around h2_capsule:decode/1 and h2_varint:decode/1, removed unreachable Router =:= undefined branch, dropped unsupported h3_datagram_enabled from quic_h3:start_server/3 opts, switched listener loop to spawn_link/3 for static reachability.
  • Configure xref_checks (skip exports_not_used for the public API) and dialyzer (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.