Embedding in an HTTP Server

View Source

The webtransport:accept/4 function lets you add WebTransport to any HTTP/3 or HTTP/2 server. Instead of start_listener/2 (which creates its own server), you call accept/4 from your existing request handler -- the same pattern as upgrading a request to WebSocket.

HTTP/3 (quic_h3)

1. Merge WT settings into your server opts

H3Opts = maps:merge(webtransport:h3_settings(), #{
    cert => CertDer,
    key => PrivateKey,
    handler => fun my_handler/5
}),
{ok, _} = quic_h3:start_server(my_server, 443, H3Opts).

h3_settings/0 returns:

KeyValue
settingsH3 SETTINGS (wt_enabled, flow-control windows, draft-02 compat)
stream_type_handlerClaims WT extension streams (0x41 bidi, 0x54 uni)
h3_datagram_enabledtrue
quic_opts#{max_datagram_frame_size => 65535, reset_stream_at => true}
connection_handlerCreates a per-connection WT stream router

Pass options to customize:

webtransport:h3_settings(#{
    compat_mode => latest,          %% default: auto
    max_data => 4194304,            %% default: 1 MB
    max_streams_bidi => 200         %% default: 100
}).

2. Route requests and accept WebTransport

my_handler(H3Conn, StreamId, <<"CONNECT">>, <<"/chat">>, Headers) ->
    {ok, _Session} = webtransport:accept(H3Conn, StreamId, Headers, #{
        transport => h3,
        handler => chat_handler,
        handler_opts => #{room => lobby}
    });

my_handler(H3Conn, StreamId, <<"CONNECT">>, <<"/game">>, Headers) ->
    {ok, _Session} = webtransport:accept(H3Conn, StreamId, Headers, #{
        transport => h3,
        handler => game_handler
    });

my_handler(H3Conn, StreamId, <<"GET">>, <<"/health">>, _Headers) ->
    quic_h3:send_response(H3Conn, StreamId, 200, []),
    quic_h3:send_data(H3Conn, StreamId, <<"ok">>, true);

my_handler(H3Conn, StreamId, _Method, _Path, _Headers) ->
    quic_h3:send_response(H3Conn, StreamId, 404, []),
    quic_h3:send_data(H3Conn, StreamId, <<"not found">>, true).

accept/4 may be called from any process, not only the one running your handler. The connection_handler installed by h3_settings/0 registers the per-connection stream router keyed by the QUIC connection pid, and accept/4 resolves the same router from its H3Conn. So if your server dispatches each request to a per-request worker, calling accept/4 from that worker still routes the session's incoming streams and datagrams correctly.

HTTP/2 (erlang_h2)

1. Merge WT settings into your server opts

H2Opts = maps:merge(webtransport:h2_settings(), #{
    cert => "cert.pem",
    key => "key.pem",
    handler => fun my_h2_handler/5
}),
{ok, _} = h2:start_server(443, H2Opts).

h2_settings/0 returns:

KeyValue
enable_connect_protocoltrue
settingsH2 SETTINGS (enableconnect_protocol, wt_initial_max*)

2. Route requests

my_h2_handler(Conn, StreamId, <<"CONNECT">>, <<"/wt">>, Headers) ->
    {ok, _Session} = webtransport:accept(Conn, StreamId, Headers, #{
        transport => h2,
        handler => my_wt_handler
    });

my_h2_handler(Conn, StreamId, <<"GET">>, Path, Headers) ->
    serve_static(Conn, StreamId, Path, Headers).

What accept/4 does

accept(Conn, StreamId, Headers, Opts) performs these steps:

  1. Runs origin_check/2 on the handler (if exported)
  2. Validates the CONNECT request (:protocol, compat mode detection for h3)
  3. Starts a webtransport_session gen_statem
  4. Registers it as the stream handler via quic_h3:set_stream_handler/3 (h3) or h2:set_stream_handler/3 (h2) -- same API the transport uses for raw streams
  5. For h3: registers with the per-connection router so extension streams (bidirectional 0x41, unidirectional 0x54) are routed to the session
  6. Sends 200 OK to the client
  7. Returns {ok, Session} or {error, Reason}

Options

KeyDefaultDescription
transporth3h3 or h2
handlerrequiredModule implementing webtransport_handler
handler_opts#{}Passed to handler:init/3
compat_modeautoh3 draft selection
max_data1048576Session flow-control window
max_streams_bidi100Max bidi streams
max_streams_uni100Max uni streams

Relationship to start_listener/2

start_listener/2 is a convenience wrapper that:

  1. Creates a quic_h3 or h2 server
  2. Installs a catch-all handler that calls accept/4 for every CONNECT
  3. Returns 405 for non-CONNECT methods

Use start_listener/2 for simple WT-only servers. Use h3_settings/0 + accept/4 when you need to:

  • Serve regular HTTP alongside WebTransport on the same port
  • Route different paths to different handlers
  • Integrate with an existing HTTP server framework