Skip to main content

src/livery_ws.erl

-module(livery_ws).
-moduledoc """
WebSocket integration on top of the `ws` library.

A handler that wants to upgrade a request to WebSocket calls
`upgrade/3` inside its body. The function performs the
protocol-specific handshake by dispatching to the adapter's
`accept_ws/4` helper. The return value is a sentinel response
(`status = 101`, `body = taken_over`) that tells `livery:emit/3`
no further bytes need to be written: the stream/socket now belongs
to the `ws` session.

```erlang
my_ws_route(Req) ->
    livery_ws:upgrade(Req, my_chat_handler, #{}).
```

`my_chat_handler` is a module implementing the `ws_handler`
behaviour (defined by `erlang_ws`).

WebSocket runs over H1 (plain `Upgrade`), H2 (RFC 8441 extended
CONNECT, via `livery_ws_h2`), and H3 (RFC 9220 extended CONNECT,
via `livery_ws_h3`).
""".

-include("livery.hrl").

-export([upgrade/3]).

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

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

-doc """
Upgrade the current request to a WebSocket session.

`HandlerMod` must implement the `ws_handler` behaviour. `Opts`
is opaque and forwarded as `HMod:init(Req, Opts)`'s second
argument by the `ws` library.

Returns a `#livery_resp{}` value:

- `status = 101, body = taken_over` on a successful handshake.
  The adapter owns nothing further on this stream after this
  point.
- `status = 400` with a textual body when the inbound headers do
  not satisfy RFC 6455.
- `status = 501` when the adapter does not support WebSocket
  upgrades (H1, H2, and H3 all do).
""".
-spec upgrade(livery_req:req(), handler_module(), handler_opts()) ->
    livery_resp:resp().
upgrade(Req, HandlerMod, Opts) ->
    Adapter = livery_req:adapter(Req),
    case adapter_supports_ws(Adapter) of
        true ->
            Stream = livery_req:stream(Req),
            case Adapter:accept_ws(Stream, Req, HandlerMod, Opts) of
                {ok, _SessionPid} ->
                    #livery_resp{status = 101, body = taken_over};
                {error, {bad_request, Why}} ->
                    livery_resp:text(
                        400,
                        iolist_to_binary([
                            <<"bad ws upgrade: ">>,
                            format_reason(Why)
                        ])
                    );
                {error, Reason} ->
                    livery_resp:text(
                        500,
                        iolist_to_binary([
                            <<"ws upgrade failed: ">>,
                            format_reason(Reason)
                        ])
                    )
            end;
        false ->
            livery_resp:text(
                501,
                <<"WebSocket upgrade not supported on this protocol">>
            )
    end.

-spec adapter_supports_ws(module()) -> boolean().
adapter_supports_ws(livery_h1) -> true;
adapter_supports_ws(livery_h2) -> true;
adapter_supports_ws(livery_h3) -> true;
adapter_supports_ws(_) -> false.

-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]).