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