{"id":50381940,"url":"https://github.com/benoitc/erlang-webtransport","last_synced_at":"2026-05-30T12:30:38.386Z","repository":{"id":354073121,"uuid":"1215427987","full_name":"benoitc/erlang-webtransport","owner":"benoitc","description":"WebTransport protocol for Erlang (HTTP/2 and HTTP/3)","archived":false,"fork":false,"pushed_at":"2026-05-28T23:01:38.000Z","size":325,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T00:16:43.921Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/benoitc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T22:32:20.000Z","updated_at":"2026-05-28T23:02:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/benoitc/erlang-webtransport","commit_stats":null,"previous_names":["benoitc/erlang-webtransport"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/benoitc/erlang-webtransport","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-webtransport","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-webtransport/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-webtransport/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-webtransport/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/benoitc","download_url":"https://codeload.github.com/benoitc/erlang-webtransport/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-webtransport/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33692997,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-30T12:30:37.536Z","updated_at":"2026-05-30T12:30:38.377Z","avatar_url":"https://github.com/benoitc.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WebTransport for Erlang\n\nAn Erlang implementation of the [WebTransport](https://www.w3.org/TR/webtransport/) protocol over:\n\n- **HTTP/3** ([draft-ietf-webtrans-http3-15](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3)) using native QUIC streams\n- **HTTP/2** ([draft-ietf-webtrans-http2-14](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http2)) using [RFC 9297](https://www.rfc-editor.org/rfc/rfc9297) capsules\n\nWebTransport provides bidirectional communication between a client and server using reliable streams and unreliable datagrams over HTTP/3 or HTTP/2.\n\n## Requirements\n\n- Erlang/OTP 26.0 or later\n- [rebar3](https://rebar3.org/)\n- OpenSSL (for certificate generation)\n\n## Installation\n\nAdd to your `rebar.config`:\n\n```erlang\n{deps, [\n    {webtransport, {git, \"https://github.com/benoitc/erlang-webtransport.git\", {branch, \"main\"}}}\n]}.\n```\n\nFetch and compile:\n\n```sh\nrebar3 get-deps\nrebar3 compile\n```\n\n## TLS certificates\n\nWebTransport requires TLS. For local development, generate a self-signed certificate:\n\n```sh\nopenssl req -x509 -newkey rsa:2048 \\\n  -keyout key.pem -out cert.pem \\\n  -days 365 -nodes -subj '/CN=localhost'\n```\n\nThis produces two files in the current directory:\n\n- `cert.pem` -- the X.509 certificate\n- `key.pem` -- the unencrypted private key\n\nFor production, use certificates from a trusted CA (e.g. [Let's Encrypt](https://letsencrypt.org/)). The `certfile` and `keyfile` options accept absolute or relative file paths.\n\n## Quick start\n\nCompile the bundled examples and start a shell with `examples/` on the code path:\n\n```sh\nerlc -o examples examples/echo_server.erl examples/echo_client.erl\nERL_FLAGS=\"-pa examples\" rebar3 shell --apps webtransport\n```\n\nThe `-pa examples` flag is required so that `echo_server` (or any other handler module you keep outside `src/`) is reachable from the session process — otherwise session init fails with `{handler_not_loaded, echo_server, nofile}`.\n\n### 1. Start the server\n\n```erlang\n{ok, _} = webtransport:start_listener(my_server, #{\n    transport =\u003e h3,\n    port =\u003e 4433,\n    certfile =\u003e \"cert.pem\",\n    keyfile =\u003e \"key.pem\",\n    handler =\u003e echo_server\n}).\n```\n\n### 2. Connect a client\n\n```erlang\n{ok, Session} = webtransport:connect(\"localhost\", 4433, \u003c\u003c\"/echo\"\u003e\u003e, #{\n    transport =\u003e h3,\n    verify =\u003e verify_none\n}).\n```\n\n### 3. Send and receive\n\n```erlang\n%% Open a bidirectional stream\n{ok, Stream} = webtransport:open_stream(Session, bidi).\n\n%% Send data\nok = webtransport:send(Session, Stream, \u003c\u003c\"hello\"\u003e\u003e).\n\n%% Receive the echo\nreceive\n    {webtransport, Session, {stream, Stream, bidi, Data}} -\u003e\n        io:format(\"Got: ~s~n\", [Data])  %% prints \"Got: echo: hello\"\nafter 3000 -\u003e\n    io:format(\"timeout~n\")\nend.\n\n%% Send a datagram (unreliable)\nok = webtransport:send_datagram(Session, \u003c\u003c\"ping\"\u003e\u003e).\n\nreceive\n    {webtransport, Session, {datagram, DgData}} -\u003e\n        io:format(\"Got: ~s~n\", [DgData])  %% prints \"Got: echo: ping\"\nafter 3000 -\u003e\n    io:format(\"timeout~n\")\nend.\n\n%% Clean up\nwebtransport:close_session(Session).\nwebtransport:stop_listener(my_server).\n```\n\n## Writing a handler\n\nHandlers implement the `webtransport_handler` behaviour. The session process calls your handler's callbacks when events occur.\n\n### Minimal handler\n\n```erlang\n-module(my_handler).\n-behaviour(webtransport_handler).\n\n-export([init/3, handle_stream/4, handle_datagram/2,\n         handle_stream_closed/3, terminate/2]).\n\ninit(_Session, _Req, _Opts) -\u003e\n    {ok, #{}}.\n\nhandle_stream(Stream, Type, Data, State) -\u003e\n    %% Echo bidi streams\n    Actions = case Type of\n        bidi -\u003e [{send, Stream, \u003c\u003c\"echo: \", Data/binary\u003e\u003e}];\n        uni  -\u003e []\n    end,\n    {ok, State, Actions}.\n\nhandle_datagram(Data, State) -\u003e\n    {ok, State, [{send_datagram, \u003c\u003c\"echo: \", Data/binary\u003e\u003e}]}.\n\nhandle_stream_closed(_Stream, _Reason, State) -\u003e\n    {ok, State}.\n\nterminate(_Reason, _State) -\u003e\n    ok.\n```\n\n### Callback reference\n\nAll callbacks receive the handler state and return `{ok, NewState}`, `{ok, NewState, Actions}`, or `{stop, Reason, NewState}`.\n\n#### `init/3` (required)\n\nCalled when a session is established.\n\n```erlang\ninit(Session, Request, Opts) -\u003e {ok, State} | {ok, State, Actions} | {error, Reason}\n```\n\n- `Session` -- the session pid (use for `webtransport:open_stream/2` etc.)\n- `Request` -- `#{path := binary(), authority := binary(), headers =\u003e [{binary(), binary()}]}`\n- `Opts` -- the `handler_opts` map from the listener or connect call\n\n`init/2` is a back-compat shim called only when `init/3` is not exported; it loses `Opts`.\n\n#### `handle_stream/4` (required)\n\nCalled when data arrives on a stream.\n\n```erlang\nhandle_stream(Stream, Type, Data, State) -\u003e {ok, State} | {ok, State, Actions} | {stop, Reason, State}\n```\n\n- `Stream` -- stream ID (integer)\n- `Type` -- `bidi` or `uni`\n- `Data` -- binary payload\n\n#### `handle_stream_fin/4` (optional)\n\nCalled when data arrives with the FIN flag (last data on the stream). If not exported, `handle_stream/4` is called instead.\n\n```erlang\nhandle_stream_fin(Stream, Type, Data, State) -\u003e {ok, State} | {ok, State, Actions} | {stop, Reason, State}\n```\n\n#### `handle_datagram/2` (required)\n\nCalled when an unreliable datagram arrives.\n\n```erlang\nhandle_datagram(Data, State) -\u003e {ok, State} | {ok, State, Actions} | {stop, Reason, State}\n```\n\n#### `handle_stream_closed/3` (required)\n\nCalled when a stream closes or is reset by the peer.\n\n```erlang\nhandle_stream_closed(Stream, Reason, State) -\u003e {ok, State} | {stop, Reason, State}\n```\n\n- `Reason` -- `normal | {reset, ErrorCode} | {error, Term} | {stop_sending, ErrorCode}`\n\n#### `handle_info/2` (optional)\n\nCalled for any Erlang message not handled by the session state machine.\n\n```erlang\nhandle_info(Info, State) -\u003e {ok, State} | {ok, State, Actions} | {stop, Reason, State}\n```\n\n#### `handle_action_failed/3` (optional)\n\nCalled when an action returned by a callback fails to dispatch (e.g. sending to an unknown stream). Default behaviour: log and continue.\n\n```erlang\nhandle_action_failed(Action, Reason, State) -\u003e {ok, State} | {stop, Reason, State}\n```\n\n#### `origin_check/2` (optional)\n\nCalled before `init/3` on server-side CONNECT requests. Return `accept` or `{reject, Status, Reason}` to refuse a session.\n\n```erlang\norigin_check(Headers, Opts) -\u003e accept | {reject, 400..599, binary()}\n```\n\nWhen not exported, the default behaviour rejects requests that carry an `origin` header (browser clients) with 403. Requests without an `origin` header (non-browser clients) are accepted. Implement this callback to allow browser origins:\n\n```erlang\norigin_check(Headers, _Opts) -\u003e\n    case proplists:get_value(\u003c\u003c\"origin\"\u003e\u003e, Headers) of\n        \u003c\u003c\"https://myapp.example.com\"\u003e\u003e -\u003e accept;\n        _ -\u003e {reject, 403, \u003c\u003c\"origin not allowed\"\u003e\u003e}\n    end.\n```\n\n#### `terminate/2` (required)\n\nCalled when the session ends.\n\n```erlang\nterminate(Reason, State) -\u003e term()\n```\n\n- `Reason` -- `normal | {closed, ErrorCode, Message} | {error, Term} | Term`\n\nWhen the peer sends `CLOSE_SESSION`, `Reason` is `{closed, ErrorCode, Message}`.\n\n### Actions\n\nCallbacks can return a list of actions as the third element of the return tuple:\n\n```erlang\nhandle_stream(Stream, bidi, Data, State) -\u003e\n    {ok, State, [\n        {send, Stream, \u003c\u003c\"echo: \", Data/binary\u003e\u003e},\n        {send_datagram, \u003c\u003c\"got data on stream\"\u003e\u003e}\n    ]}.\n```\n\n| Action | Description |\n|--------|-------------|\n| `{send, Stream, Data}` | Send data on a stream |\n| `{send, Stream, Data, fin}` | Send data and half-close the stream |\n| `{send_datagram, Data}` | Send an unreliable datagram |\n| `{open_stream, bidi \\| uni}` | Open a new stream |\n| `{close_stream, Stream}` | Half-close a stream (send FIN) |\n| `{reset_stream, Stream, ErrorCode}` | Abort a stream with an error code |\n| `{stop_sending, Stream, ErrorCode}` | Ask the peer to stop sending on a stream |\n| `drain_session` | Signal that no new streams will be opened |\n| `{close_session, ErrorCode, Reason}` | Close the session |\n\n## Server API\n\n### Starting a listener\n\n```erlang\n{ok, Pid} = webtransport:start_listener(Name, Opts).\n```\n\n`Name` is an atom used to identify the listener. `Opts` is a map:\n\n| Option | Required | Default | Description |\n|--------|----------|---------|-------------|\n| `transport` | yes | -- | `h2` (HTTP/2) or `h3` (HTTP/3) |\n| `port` | yes | -- | TCP/UDP port to listen on |\n| `certfile` | yes | -- | Path to TLS certificate (PEM) |\n| `keyfile` | yes | -- | Path to TLS private key (PEM) |\n| `handler` | yes | -- | Module implementing `webtransport_handler` |\n| `handler_opts` | no | `#{}` | Map passed to `handler:init/3` as the third argument |\n| `max_data` | no | 1048576 (1 MB) | Session-level flow-control window (bytes) |\n| `max_streams_bidi` | no | 100 | Max concurrent bidirectional streams |\n| `max_streams_uni` | no | 100 | Max concurrent unidirectional streams |\n| `compat_mode` | no | `auto` | HTTP/3 draft selection (see [Compatibility](#compatibility-mode)) |\n\n### Managing listeners\n\n```erlang\n%% Stop a listener\nok = webtransport:stop_listener(Name).\n\n%% List active listeners\n[Name] = webtransport:listeners().\n\n%% Get listener info\n{ok, #{transport := h3, port := 4433, handler := my_handler}} =\n    webtransport:listener_info(Name).\n```\n\n## Embedding in an HTTP server\n\nUse `accept/4` to add WebTransport to an existing HTTP/3 or HTTP/2 server.\nYour server owns the listener and routing; `accept/4` upgrades a specific\nCONNECT request into a WebTransport session -- the same pattern as WebSocket\nupgrade.\n\n### HTTP/3 example\n\n```erlang\n%% 1. Merge WT config into your quic_h3 server\nH3Opts = maps:merge(webtransport:h3_settings(), #{\n    cert =\u003e CertDer, key =\u003e PrivateKey,\n    handler =\u003e fun my_handler/5\n}),\n{ok, _} = quic_h3:start_server(my_server, 443, H3Opts).\n\n%% 2. In your request handler, route and upgrade\nmy_handler(H3Conn, StreamId, \u003c\u003c\"CONNECT\"\u003e\u003e, \u003c\u003c\"/chat\"\u003e\u003e, Headers) -\u003e\n    {ok, _Session} = webtransport:accept(H3Conn, StreamId, Headers, #{\n        transport =\u003e h3,\n        handler =\u003e chat_handler,\n        handler_opts =\u003e #{room =\u003e lobby}\n    });\nmy_handler(H3Conn, StreamId, \u003c\u003c\"CONNECT\"\u003e\u003e, \u003c\u003c\"/game\"\u003e\u003e, Headers) -\u003e\n    {ok, _Session} = webtransport:accept(H3Conn, StreamId, Headers, #{\n        transport =\u003e h3,\n        handler =\u003e game_handler\n    });\nmy_handler(H3Conn, StreamId, \u003c\u003c\"GET\"\u003e\u003e, _Path, _Headers) -\u003e\n    quic_h3:send_response(H3Conn, StreamId, 200, []),\n    quic_h3:send_data(H3Conn, StreamId, \u003c\u003c\"hello\"\u003e\u003e, true).\n```\n\n### HTTP/2 example\n\n```erlang\nH2Opts = maps:merge(webtransport:h2_settings(), #{\n    cert =\u003e \"cert.pem\", key =\u003e \"key.pem\",\n    handler =\u003e fun my_h2_handler/5\n}),\n{ok, _} = h2:start_server(443, H2Opts).\n\nmy_h2_handler(Conn, StreamId, \u003c\u003c\"CONNECT\"\u003e\u003e, \u003c\u003c\"/wt\"\u003e\u003e, Headers) -\u003e\n    {ok, _Session} = webtransport:accept(Conn, StreamId, Headers, #{\n        transport =\u003e h2,\n        handler =\u003e my_wt_handler\n    });\nmy_h2_handler(Conn, StreamId, \u003c\u003c\"GET\"\u003e\u003e, Path, Headers) -\u003e\n    serve_static(Conn, StreamId, Path, Headers).\n```\n\n### accept/4 options\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `transport` | `h3` | `h3` or `h2` |\n| `handler` | required | Module implementing `webtransport_handler` |\n| `handler_opts` | `#{}` | Passed to `handler:init/3` |\n| `compat_mode` | `auto` | HTTP/3 draft selection |\n| `max_data` | 1048576 | Session flow-control window |\n| `max_streams_bidi` | 100 | Max bidi streams |\n| `max_streams_uni` | 100 | Max uni streams |\n\n`accept/4` validates the CONNECT headers, starts a session, registers it\nas the stream handler (same as `quic_h3:set_stream_handler/3`), sends 200,\nand returns `{ok, Session}`. The session pid works with all session API\nfunctions (`send/3`, `open_stream/2`, etc.).\n\nSee the [Integration guide](docs/integration.md) for details.\n\n## Client API\n\n### Connecting\n\n```erlang\n{ok, Session} = webtransport:connect(Host, Port, Path, Opts).\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `transport` | `h3` | `h2` or `h3` |\n| `verify` | `verify_peer` | `verify_peer` or `verify_none` |\n| `cacertfile` | -- | Path to CA certificate bundle for peer verification |\n| `certfile` | -- | Client certificate (mutual TLS) |\n| `keyfile` | -- | Client private key (mutual TLS) |\n| `headers` | `[]` | Extra headers on the CONNECT request |\n| `timeout` | 30000 | Connection timeout in milliseconds |\n| `handler_opts` | `#{}` | Map passed to the handler's `init/3` |\n| `compat_mode` | `latest` | HTTP/3 draft selection (see [Compatibility](#compatibility-mode)) |\n\n### Connecting with a custom handler\n\n```erlang\n{ok, Session} = webtransport:connect(Host, Port, Path, Opts, MyHandler).\n```\n\nWhen no handler is given, `webtransport_client_handler` is used. It forwards all events to the calling process as messages.\n\n### Default client messages\n\nWhen using the default handler, the process that called `connect/4` receives:\n\n| Message | Description |\n|---------|-------------|\n| `{webtransport, Session, {stream, Stream, Type, Data}}` | Stream data received |\n| `{webtransport, Session, {stream_fin, Stream, Type, Data}}` | Stream data with FIN |\n| `{webtransport, Session, {datagram, Data}}` | Datagram received |\n| `{webtransport, Session, {stream_closed, Stream, Reason}}` | Stream closed |\n| `{webtransport, Session, closed}` | Session terminated |\n\n## Session API\n\nOnce connected, use these functions on the session pid:\n\n```erlang\n%% Streams\n{ok, Stream} = webtransport:open_stream(Session, bidi).  %% or uni\nok = webtransport:send(Session, Stream, Data).\nok = webtransport:send(Session, Stream, Data, fin).\nok = webtransport:close_stream(Session, Stream).\nok = webtransport:reset_stream(Session, Stream, ErrorCode).\nok = webtransport:stop_sending(Session, Stream, ErrorCode).\n\n%% Datagrams\nok = webtransport:send_datagram(Session, Data).\n\n%% Session lifecycle\nok = webtransport:drain_session(Session).\nok = webtransport:close_session(Session).\nok = webtransport:close_session(Session, ErrorCode).\nok = webtransport:close_session(Session, ErrorCode, Reason).\n\n%% Introspection\n{ok, Info} = webtransport:session_info(Session).\n%% Info :: #{transport, stream_count, local_max_data, remote_max_data,\n%%           local_max_streams_bidi, local_max_streams_uni,\n%%           remote_max_streams_bidi, remote_max_streams_uni,\n%%           bytes_sent, bytes_received, close_info =\u003e {Code, Msg}}\n```\n\n## Compatibility mode\n\nThe HTTP/3 WebTransport spec has evolved through multiple drafts. As of April 2026, Safari and the IETF are on draft-15; Chrome and Firefox still use draft-02. This library keeps the two paths disjoint:\n\n| Mode | `:protocol` | SETTINGS | Use when |\n|------|-------------|----------|----------|\n| `latest` | `webtransport-h3` | `wt_enabled=1` + initial flow-control | Talking to draft-15 peers (Safari, spec-conformant servers) |\n| `legacy_browser_compat` | `webtransport` | `SETTINGS_ENABLE_WEBTRANSPORT_DRAFT02=1` | Talking to draft-02 peers (Chrome, Firefox, quic-go v0.9) |\n| `auto` (server only) | accepts both | advertises both | Let the server accept either draft based on the client's request |\n\n**Server default:** `auto` -- the server inspects `:protocol` and the `Sec-Webtransport-Http3-Draft02` header on each CONNECT request and dispatches to the matching code path. Pin to `latest` or `legacy_browser_compat` to refuse the other:\n\n```erlang\n%% Accept only draft-15 clients\nwebtransport:start_listener(strict, #{\n    transport =\u003e h3,\n    port =\u003e 4433,\n    certfile =\u003e \"cert.pem\",\n    keyfile =\u003e \"key.pem\",\n    handler =\u003e my_handler,\n    compat_mode =\u003e latest\n}).\n```\n\n**Client default:** `latest`. To connect to a draft-02 server:\n\n```erlang\n{ok, Session} = webtransport:connect(\"example.com\", 443, \u003c\u003c\"/wt\"\u003e\u003e, #{\n    transport =\u003e h3,\n    compat_mode =\u003e legacy_browser_compat,\n    verify =\u003e verify_none\n}).\n```\n\nHTTP/2 WebTransport (`transport =\u003e h2`) has no draft-02 variant; `compat_mode` applies only to HTTP/3.\n\n## Flow control\n\nWebTransport provides session-level and per-stream flow control. Defaults:\n\n| Parameter | Default | Description |\n|-----------|---------|-------------|\n| `max_data` | 1 MB | Session-level byte limit |\n| `max_streams_bidi` | 100 | Max concurrent bidirectional streams |\n| `max_streams_uni` | 100 | Max concurrent unidirectional streams |\n\nOverride at listener or connect time:\n\n```erlang\nwebtransport:start_listener(my_server, #{\n    transport =\u003e h3,\n    port =\u003e 4433,\n    certfile =\u003e \"cert.pem\",\n    keyfile =\u003e \"key.pem\",\n    handler =\u003e my_handler,\n    max_data =\u003e 4194304,        %% 4 MB\n    max_streams_bidi =\u003e 200,\n    max_streams_uni =\u003e 50\n}).\n```\n\nThe library enforces:\n\n- **Monotonicity** -- a peer sending a decreased `WT_MAX_DATA` or `WT_MAX_STREAMS` closes the session with `WT_FLOW_CONTROL_ERROR`.\n- **Peer stream count** -- streams opened beyond the advertised limit are rejected with `WT_BUFFERED_STREAM_REJECTED`.\n- **HTTP/3 prohibition** -- `WT_MAX_STREAM_DATA` and `WT_STREAM_DATA_BLOCKED` capsules are session errors on HTTP/3 (per-stream flow control uses native QUIC).\n- **HTTP/2 WebTransport-Init** -- the `WebTransport-Init` structured-field header ([draft-14 section 4.3.2](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http2-14#section-4.3.2)) carries initial flow-control windows. When both SETTINGS and the header are present, the greater value is used.\n\n## Datagram limits\n\nDatagrams are bounded by the transport:\n\n| Transport | Max payload | Reason |\n|-----------|-------------|--------|\n| HTTP/3 | 65527 bytes | `max_datagram_frame_size` (65535) minus session-id varint (up to 8 bytes) |\n| HTTP/2 | 65471 bytes | HTTP/2 initial stream window (65535) minus capsule framing overhead (64 bytes) |\n\nSending a datagram larger than the limit returns `{error, datagram_too_large}`.\n\n## Error codes\n\nThe library uses draft-defined error codes:\n\n| Constant | Value | Meaning |\n|----------|-------|---------|\n| `WT_BUFFERED_STREAM_REJECTED` | `0x3994bd84` | Peer exceeded buffered stream limit |\n| `WT_SESSION_GONE` | `0x170d7b68` | Session terminated; stream belongs to closed session |\n| `WT_FLOW_CONTROL_ERROR` | `0x045d4487` | Flow-control violation (e.g. decreased limit) |\n| `WT_REQUIREMENTS_NOT_MET` | `0x212c0d48` | Protocol requirements not satisfied |\n\nApplication-level error codes are mapped to/from QUIC error codes per [draft-15 section 3.3](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15#section-3.3).\n\n## Session termination\n\nWhen a session closes (locally or by the peer):\n\n1. All live streams are reset with `WT_SESSION_GONE`.\n2. A `CLOSE_SESSION` capsule is sent (or received) with an error code and reason (max 1024 bytes).\n3. The CONNECT stream is half-closed (FIN sent).\n4. The handler's `terminate/2` receives `{closed, ErrorCode, Reason}` as the reason.\n\nIf the peer FINs the CONNECT stream without sending `CLOSE_SESSION`, the session terminates with `{closed, 0, \u003c\u003c\"peer closed CONNECT\"\u003e\u003e}`.\n\n## Architecture\n\n```\nwebtransport            Public API (connect, send, open_stream, ...)\n    |\nwebtransport_session    gen_statem per session (flow control, handler dispatch)\n    |\n    +-- webtransport_h3     HTTP/3 transport (QUIC streams + datagrams)\n    |     +-- wt_h3              Settings, headers, peer validation\n    |     +-- wt_h3_capsule      CLOSE/DRAIN capsule encode/decode\n    |     +-- webtransport_h3_router   Per-connection stream demux\n    |\n    +-- webtransport_h2     HTTP/2 transport (capsules over CONNECT stream)\n    |     +-- wt_h2_capsule      All 14 capsule types encode/decode\n    |     +-- wt_h2_init         WebTransport-Init header parse/encode\n    |\n    +-- webtransport_stream     Per-stream state (flow control, buffers)\n    +-- wt_error                App error code mapping (draft-15 section 3.3)\n    +-- webtransport_handler    Behaviour definition\n```\n\n## Examples\n\nThe `examples/` directory contains a working echo server and client.\n\nCompile and run them:\n\n```sh\n# Generate certs (if not done already)\nopenssl req -x509 -newkey rsa:2048 \\\n  -keyout key.pem -out cert.pem \\\n  -days 365 -nodes -subj '/CN=localhost'\n\n# Compile examples\nerlc -o examples \\\n  -pa _build/default/lib/webtransport/ebin \\\n  -pa _build/default/lib/quic/ebin \\\n  -pa _build/default/lib/erlang_h2/ebin \\\n  -I include \\\n  examples/echo_server.erl examples/echo_client.erl\n\n# Start a shell\nERL_FLAGS=\"-pa examples\" rebar3 shell --apps webtransport\n```\n\n```erlang\n%% Start the echo server\necho_server:start(4433).\n\n%% Run the echo client tests\necho_client:test(\"localhost\", 4433).\n```\n\n## Testing\n\n```sh\n# Unit tests (298 tests)\nrebar3 eunit\n\n# Integration tests (54 tests, both h2 and h3)\nrebar3 ct --suite=test/webtransport_SUITE\n\n# Docker interop (erlang vs erlang)\ncd interop \u0026\u0026 docker compose up --abort-on-container-exit --build\n\n# Cross-implementation interop (erlang vs webtransport-go)\n./scripts/interop_cross.sh\n```\n\n## Specifications\n\n- [draft-ietf-webtrans-http3-15](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3) -- WebTransport over HTTP/3\n- [draft-ietf-webtrans-http2-14](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http2) -- WebTransport over HTTP/2\n- [RFC 9297](https://www.rfc-editor.org/rfc/rfc9297) -- HTTP Datagrams and the Capsule Protocol\n- [RFC 9000](https://www.rfc-editor.org/rfc/rfc9000) -- QUIC: A UDP-Based Multiplexed and Secure Transport\n- [RFC 9114](https://www.rfc-editor.org/rfc/rfc9114) -- HTTP/3\n- [RFC 8441](https://www.rfc-editor.org/rfc/rfc8441) -- Bootstrapping WebSockets with HTTP/2 (Extended CONNECT)\n- [W3C WebTransport API](https://www.w3.org/TR/webtransport/) -- Browser API specification\n\n## Sponsors\n\n\u003ca href=\"https://enki-multimedia.eu\"\u003e\u003cimg src=\"docs/images/enki-multimedia.svg\" alt=\"Enki Multimedia\" height=\"50\" /\u003e\u003c/a\u003e\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbenoitc%2Ferlang-webtransport","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbenoitc%2Ferlang-webtransport","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbenoitc%2Ferlang-webtransport/lists"}