mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Add a commands-based interface to Websocket handlers
This feature is currently experimental. It will become the preferred way to use Websocket handlers once it becomes documented. A commands-based interface enables adding commands without having to change the interface much. It mirrors the interface of stream handlers or gen_statem. It will enable adding commands that have been needed for some time but were not implemented for fear of making the interface too complex.
This commit is contained in:
parent
4b385749f2
commit
8404b1c908
5 changed files with 342 additions and 4 deletions
31
test/handlers/ws_handle_commands_h.erl
Normal file
31
test/handlers/ws_handle_commands_h.erl
Normal file
|
@ -0,0 +1,31 @@
|
|||
%% This module takes commands from the x-commands header
|
||||
%% and returns them in the websocket_handle/2 callback.
|
||||
|
||||
-module(ws_handle_commands_h).
|
||||
-behavior(cowboy_websocket).
|
||||
|
||||
-export([init/2]).
|
||||
-export([websocket_init/1]).
|
||||
-export([websocket_handle/2]).
|
||||
-export([websocket_info/2]).
|
||||
|
||||
init(Req=#{pid := Pid}, RunOrHibernate) ->
|
||||
Commands0 = cowboy_req:header(<<"x-commands">>, Req),
|
||||
Commands = binary_to_term(base64:decode(Commands0)),
|
||||
case Commands of
|
||||
bad -> ct_helper_error_h:ignore(Pid, cowboy_websocket, handler_call, 6);
|
||||
_ -> ok
|
||||
end,
|
||||
{cowboy_websocket, Req, {Commands, RunOrHibernate}}.
|
||||
|
||||
websocket_init(State) ->
|
||||
{[], State}.
|
||||
|
||||
websocket_handle(_, State={Commands, run}) ->
|
||||
{Commands, State};
|
||||
websocket_handle(_, State={Commands, hibernate}) ->
|
||||
{Commands, State, hibernate}.
|
||||
|
||||
websocket_info(_, State) ->
|
||||
{[], State}.
|
||||
|
32
test/handlers/ws_info_commands_h.erl
Normal file
32
test/handlers/ws_info_commands_h.erl
Normal file
|
@ -0,0 +1,32 @@
|
|||
%% This module takes commands from the x-commands header
|
||||
%% and returns them in the websocket_info/2 callback.
|
||||
%% This callback is triggered via a message.
|
||||
|
||||
-module(ws_info_commands_h).
|
||||
-behavior(cowboy_websocket).
|
||||
|
||||
-export([init/2]).
|
||||
-export([websocket_init/1]).
|
||||
-export([websocket_handle/2]).
|
||||
-export([websocket_info/2]).
|
||||
|
||||
init(Req=#{pid := Pid}, RunOrHibernate) ->
|
||||
Commands0 = cowboy_req:header(<<"x-commands">>, Req),
|
||||
Commands = binary_to_term(base64:decode(Commands0)),
|
||||
case Commands of
|
||||
bad -> ct_helper_error_h:ignore(Pid, cowboy_websocket, handler_call, 6);
|
||||
_ -> ok
|
||||
end,
|
||||
{cowboy_websocket, Req, {Commands, RunOrHibernate}}.
|
||||
|
||||
websocket_init(State) ->
|
||||
self() ! shoot,
|
||||
{[], State}.
|
||||
|
||||
websocket_handle(_, State) ->
|
||||
{[], State}.
|
||||
|
||||
websocket_info(_, State={Commands, run}) ->
|
||||
{Commands, State};
|
||||
websocket_info(_, State={Commands, hibernate}) ->
|
||||
{Commands, State, hibernate}.
|
30
test/handlers/ws_init_commands_h.erl
Normal file
30
test/handlers/ws_init_commands_h.erl
Normal file
|
@ -0,0 +1,30 @@
|
|||
%% This module takes commands from the x-commands header
|
||||
%% and returns them in the websocket_init/1 callback.
|
||||
|
||||
-module(ws_init_commands_h).
|
||||
-behavior(cowboy_websocket).
|
||||
|
||||
-export([init/2]).
|
||||
-export([websocket_init/1]).
|
||||
-export([websocket_handle/2]).
|
||||
-export([websocket_info/2]).
|
||||
|
||||
init(Req=#{pid := Pid}, RunOrHibernate) ->
|
||||
Commands0 = cowboy_req:header(<<"x-commands">>, Req),
|
||||
Commands = binary_to_term(base64:decode(Commands0)),
|
||||
case Commands of
|
||||
bad -> ct_helper_error_h:ignore(Pid, cowboy_websocket, handler_call, 6);
|
||||
_ -> ok
|
||||
end,
|
||||
{cowboy_websocket, Req, {Commands, RunOrHibernate}}.
|
||||
|
||||
websocket_init(State={Commands, run}) ->
|
||||
{Commands, State};
|
||||
websocket_init(State={Commands, hibernate}) ->
|
||||
{Commands, State, hibernate}.
|
||||
|
||||
websocket_handle(_, State) ->
|
||||
{[], State}.
|
||||
|
||||
websocket_info(_, State) ->
|
||||
{[], State}.
|
207
test/ws_handler_SUITE.erl
Normal file
207
test/ws_handler_SUITE.erl
Normal file
|
@ -0,0 +1,207 @@
|
|||
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(ws_handler_SUITE).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-import(ct_helper, [config/2]).
|
||||
-import(ct_helper, [doc/1]).
|
||||
-import(cowboy_test, [gun_open/1]).
|
||||
-import(cowboy_test, [gun_down/1]).
|
||||
|
||||
%% ct.
|
||||
|
||||
all() ->
|
||||
[{group, ws}, {group, ws_hibernate}].
|
||||
|
||||
groups() ->
|
||||
AllTests = ct_helper:all(?MODULE),
|
||||
[{ws, [parallel], AllTests}, {ws_hibernate, [parallel], AllTests}].
|
||||
|
||||
init_per_group(Name, Config) ->
|
||||
cowboy_test:init_http(Name, #{
|
||||
env => #{dispatch => init_dispatch(Name)}
|
||||
}, Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
|
||||
%% Dispatch configuration.
|
||||
|
||||
init_dispatch(Name) ->
|
||||
RunOrHibernate = case Name of
|
||||
ws -> run;
|
||||
ws_hibernate -> hibernate
|
||||
end,
|
||||
cowboy_router:compile([{'_', [
|
||||
{"/init", ws_init_commands_h, RunOrHibernate},
|
||||
{"/handle", ws_handle_commands_h, RunOrHibernate},
|
||||
{"/info", ws_info_commands_h, RunOrHibernate}
|
||||
]}]).
|
||||
|
||||
%% Support functions for testing using Gun.
|
||||
|
||||
gun_open_ws(Config, Path, Commands) ->
|
||||
ConnPid = gun_open(Config),
|
||||
StreamRef = gun:ws_upgrade(ConnPid, Path, [
|
||||
{<<"x-commands">>, base64:encode(term_to_binary(Commands))}
|
||||
]),
|
||||
receive
|
||||
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
|
||||
{ok, ConnPid, StreamRef};
|
||||
{gun_response, ConnPid, _, _, Status, Headers} ->
|
||||
exit({ws_upgrade_failed, Status, Headers});
|
||||
{gun_error, ConnPid, StreamRef, Reason} ->
|
||||
exit({ws_upgrade_failed, Reason})
|
||||
after 1000 ->
|
||||
error(timeout)
|
||||
end.
|
||||
|
||||
receive_ws(ConnPid, StreamRef) ->
|
||||
receive
|
||||
{gun_ws, ConnPid, StreamRef, Frame} ->
|
||||
{ok, Frame}
|
||||
after 1000 ->
|
||||
{error, timeout}
|
||||
end.
|
||||
|
||||
ensure_handle_is_called(ConnPid, "/handle") ->
|
||||
gun:ws_send(ConnPid, {text, <<"Necessary to trigger websocket_handle/2.">>});
|
||||
ensure_handle_is_called(_, _) ->
|
||||
ok.
|
||||
|
||||
%% Tests.
|
||||
|
||||
websocket_init_nothing(Config) ->
|
||||
doc("Nothing happens when websocket_init/1 returns no commands."),
|
||||
do_nothing(Config, "/init").
|
||||
|
||||
websocket_handle_nothing(Config) ->
|
||||
doc("Nothing happens when websocket_handle/2 returns no commands."),
|
||||
do_nothing(Config, "/handle").
|
||||
|
||||
websocket_info_nothing(Config) ->
|
||||
doc("Nothing happens when websocket_info/2 returns no commands."),
|
||||
do_nothing(Config, "/info").
|
||||
|
||||
do_nothing(Config, Path) ->
|
||||
{ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, []),
|
||||
ensure_handle_is_called(ConnPid, Path),
|
||||
{error, timeout} = receive_ws(ConnPid, StreamRef),
|
||||
ok.
|
||||
|
||||
websocket_init_invalid(Config) ->
|
||||
doc("The connection must be closed when websocket_init/1 returns an invalid command."),
|
||||
do_invalid(Config, "/init").
|
||||
|
||||
websocket_handle_invalid(Config) ->
|
||||
doc("The connection must be closed when websocket_handle/2 returns an invalid command."),
|
||||
do_invalid(Config, "/init").
|
||||
|
||||
websocket_info_invalid(Config) ->
|
||||
doc("The connection must be closed when websocket_info/2 returns an invalid command."),
|
||||
do_invalid(Config, "/info").
|
||||
|
||||
do_invalid(Config, Path) ->
|
||||
{ok, ConnPid, _} = gun_open_ws(Config, Path, bad),
|
||||
ensure_handle_is_called(ConnPid, Path),
|
||||
gun_down(ConnPid).
|
||||
|
||||
websocket_init_one_frame(Config) ->
|
||||
doc("A single frame is received when websocket_init/1 returns it as a command."),
|
||||
do_one_frame(Config, "/init").
|
||||
|
||||
websocket_handle_one_frame(Config) ->
|
||||
doc("A single frame is received when websocket_handle/2 returns it as a command."),
|
||||
do_one_frame(Config, "/handle").
|
||||
|
||||
websocket_info_one_frame(Config) ->
|
||||
doc("A single frame is received when websocket_info/2 returns it as a command."),
|
||||
do_one_frame(Config, "/info").
|
||||
|
||||
do_one_frame(Config, Path) ->
|
||||
{ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
|
||||
{text, <<"One frame!">>}
|
||||
]),
|
||||
ensure_handle_is_called(ConnPid, Path),
|
||||
{ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
|
||||
ok.
|
||||
|
||||
websocket_init_many_frames(Config) ->
|
||||
doc("Multiple frames are received when websocket_init/1 returns them as commands."),
|
||||
do_many_frames(Config, "/init").
|
||||
|
||||
websocket_handle_many_frames(Config) ->
|
||||
doc("Multiple frames are received when websocket_handle/2 returns them as commands."),
|
||||
do_many_frames(Config, "/handle").
|
||||
|
||||
websocket_info_many_frames(Config) ->
|
||||
doc("Multiple frames are received when websocket_info/2 returns them as commands."),
|
||||
do_many_frames(Config, "/info").
|
||||
|
||||
do_many_frames(Config, Path) ->
|
||||
{ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
|
||||
{text, <<"One frame!">>},
|
||||
{binary, <<"Two frames!">>}
|
||||
]),
|
||||
ensure_handle_is_called(ConnPid, Path),
|
||||
{ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
|
||||
{ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef),
|
||||
ok.
|
||||
|
||||
websocket_init_close_frame(Config) ->
|
||||
doc("A single close frame is received when websocket_init/1 returns it as a command."),
|
||||
do_close_frame(Config, "/init").
|
||||
|
||||
websocket_handle_close_frame(Config) ->
|
||||
doc("A single close frame is received when websocket_handle/2 returns it as a command."),
|
||||
do_close_frame(Config, "/handle").
|
||||
|
||||
websocket_info_close_frame(Config) ->
|
||||
doc("A single close frame is received when websocket_info/2 returns it as a command."),
|
||||
do_close_frame(Config, "/info").
|
||||
|
||||
do_close_frame(Config, Path) ->
|
||||
{ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [close]),
|
||||
ensure_handle_is_called(ConnPid, Path),
|
||||
{ok, close} = receive_ws(ConnPid, StreamRef),
|
||||
gun_down(ConnPid).
|
||||
|
||||
websocket_init_many_frames_then_close_frame(Config) ->
|
||||
doc("Multiple frames are received followed by a close frame "
|
||||
"when websocket_init/1 returns them as commands."),
|
||||
do_many_frames_then_close_frame(Config, "/init").
|
||||
|
||||
websocket_handle_many_frames_then_close_frame(Config) ->
|
||||
doc("Multiple frames are received followed by a close frame "
|
||||
"when websocket_handle/2 returns them as commands."),
|
||||
do_many_frames_then_close_frame(Config, "/handle").
|
||||
|
||||
websocket_info_many_frames_then_close_frame(Config) ->
|
||||
doc("Multiple frames are received followed by a close frame "
|
||||
"when websocket_info/2 returns them as commands."),
|
||||
do_many_frames_then_close_frame(Config, "/info").
|
||||
|
||||
do_many_frames_then_close_frame(Config, Path) ->
|
||||
{ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
|
||||
{text, <<"One frame!">>},
|
||||
{binary, <<"Two frames!">>},
|
||||
close
|
||||
]),
|
||||
ensure_handle_is_called(ConnPid, Path),
|
||||
{ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
|
||||
{ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef),
|
||||
{ok, close} = receive_ws(ConnPid, StreamRef),
|
||||
gun_down(ConnPid).
|
Loading…
Add table
Add a link
Reference in a new issue