Skip to main content

src/livery_wt.erl

-module(livery_wt).
-moduledoc """
WebTransport integration on top of the `webtransport` library.

A handler that wants to accept a WebTransport session (initiated
by an HTTP extended CONNECT with `:protocol = webtransport`) calls
`upgrade/3`. The function dispatches to the adapter's `accept_wt/4`
helper, which reconstructs the CONNECT pseudo-headers and calls
`webtransport:accept/4`. On success the stream/session belongs to
the `webtransport` session process and `upgrade/3` returns the
`taken_over` sentinel response.

```erlang
my_wt_route(Req) ->
    livery_wt:upgrade(Req, my_wt_handler, #{}).
```

`my_wt_handler` implements the `webtransport_handler` behaviour
(from `erlang-webtransport`).

WebTransport runs only over H2 (extended CONNECT, RFC 9220-style)
and H3. Calling `upgrade/3` on H1 returns `501 Not Implemented`.

The listener must advertise the WebTransport settings. Merge
`webtransport:h3_settings/0` (or `h2_settings/0`) into the listener
options so the adapter forwards the H3/H2 SETTINGS, datagram
support, and WT stream routing to the wire library:

```erlang
Opts = maps:merge(webtransport:h3_settings(), #{
    cert => Cert, key => Key, stack => Stack,
    handler => fun(Req) -> livery_wt:upgrade(Req, my_handler, #{}) end
}),
livery_h3:start(Opts).
```

End-to-end bidi-stream and datagram echo over a real session is
covered by `livery_wt_SUITE` (needs `webtransport` >= 0.2.3, where
`accept/4` works from Livery's per-request worker process).
""".

-include("livery.hrl").

-export([upgrade/3]).

-export_type([handler_module/0, handler_opts/0]).

-type handler_module() :: module().
-type handler_opts() :: term().

-doc """
Accept a WebTransport session on the current request.

`HandlerMod` implements `webtransport_handler`. Returns the
`taken_over` sentinel on success, `501` on H1, or `4xx/5xx` text
when the handshake is rejected.
""".
-spec upgrade(livery_req:req(), handler_module(), handler_opts()) ->
    livery_resp:resp().
upgrade(Req, HandlerMod, Opts) ->
    Adapter = livery_req:adapter(Req),
    case adapter_transport(Adapter) of
        undefined ->
            livery_resp:text(
                501,
                <<"WebTransport not supported on this protocol">>
            );
        Transport ->
            case Adapter:accept_wt(Transport, Req, HandlerMod, Opts) of
                {ok, _Session} ->
                    #livery_resp{status = 200, body = taken_over};
                {error, not_connect_method} ->
                    livery_resp:text(400, <<"not a CONNECT request">>);
                {error, {rejected, Status}} ->
                    livery_resp:text(Status, <<"webtransport rejected">>);
                {error, Reason} ->
                    livery_resp:text(
                        500,
                        iolist_to_binary([
                            <<"webtransport upgrade failed: ">>,
                            format_reason(Reason)
                        ])
                    )
            end
    end.

-spec adapter_transport(module()) -> h2 | h3 | undefined.
adapter_transport(livery_h2) -> h2;
adapter_transport(livery_h3) -> h3;
adapter_transport(_) -> undefined.

-spec format_reason(term()) -> iodata().
format_reason(B) when is_binary(B) -> B;
format_reason(A) when is_atom(A) -> atom_to_binary(A);
format_reason(Other) -> io_lib:format("~p", [Other]).