0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 20:30:23 +00:00

Allow passing options to sub protocols

Before this commit we had an issue where configuring a
Websocket connection was simply not possible without
doing magic, adding callbacks or extra return values.
The init/2 function only allowed setting hibernate
and timeout options.

After this commit, when switching to a different
type of handler you can either return

  {module, Req, State}

or

  {module, Req, State, Opts}

where Opts is any value (as far as the sub protocol
interface is concerned) and is ultimately checked
by the custom handlers.

A large protocol like Websocket would accept only
a map there, with many different options, while a
small interface like loop handlers would allow
passing hibernate and nothing else.

For Websocket, hibernate must be set from the
websocket_init/1 callback, because init/2 executes
in a separate process.

Sub protocols now have two callbacks: one with the
Opts value, one without.

The loop handler code was largely reworked and
simplified. It does not need to manage a timeout
or read from the socket anymore, it's the job of
the protocol code. A lot of unnecessary stuff was
therefore removed.

Websocket compression must now be enabled from
the handler options instead of per listener. This
means that a project can have two separate Websocket
handlers with different options. Compression is
still disabled by default, and the idle_timeout
value was changed from inifnity to 60000 (60 seconds),
as that's safer and is also a good value for mobile
devices.
This commit is contained in:
Loïc Hoguin 2017-02-18 18:26:20 +01:00
parent 80f8cda7ff
commit a45813c60f
No known key found for this signature in database
GPG key ID: 71366FF21851DF03
25 changed files with 171 additions and 244 deletions

View file

@ -34,7 +34,7 @@ loop handler behavior. This tuple may optionally contain
a timeout value and/or the atom `hibernate` to make the a timeout value and/or the atom `hibernate` to make the
process enter hibernation until a message is received. process enter hibernation until a message is received.
This snippet enables the loop handler. This snippet enables the loop handler:
[source,erlang] [source,erlang]
---- ----
@ -42,14 +42,12 @@ init(Req, State) ->
{cowboy_loop, Req, State}. {cowboy_loop, Req, State}.
---- ----
However it is largely recommended that you set a timeout This also makes the process hibernate:
value. The next example sets a timeout value of 30s and
also makes the process hibernate.
[source,erlang] [source,erlang]
---- ----
init(Req, State) -> init(Req, State) ->
{cowboy_loop, Req, State, 30000, hibernate}. {cowboy_loop, Req, State, hibernate}.
---- ----
=== Receive loop === Receive loop
@ -123,25 +121,6 @@ a subsequent request.
Please refer to the xref:handlers[Handlers chapter] Please refer to the xref:handlers[Handlers chapter]
for general instructions about cleaning up. for general instructions about cleaning up.
=== Timeout
Note that this feature currently does not work. It will be
brought back in a future 2.0 pre-release.
By default Cowboy will not attempt to close the connection
if there is no activity from the client. This is not always
desirable, which is why you can set a timeout. Cowboy will
close the connection if no data was received from the client
after the configured time. The timeout only needs to be set
once and can't be modified afterwards.
Because the request may have had a body, or may be followed
by another request, Cowboy is forced to buffer all data it
receives. This data may grow to become too large though,
so there is a configurable limit for it. The default buffer
size is of 5000 bytes, but it may be changed by setting the
`loop_max_buffer` middleware environment value.
=== Hibernate === Hibernate
To save memory, you may hibernate the process in between To save memory, you may hibernate the process in between

View file

@ -20,31 +20,31 @@ init(Req, State) ->
{cowboy_websocket, Req, State}. {cowboy_websocket, Req, State}.
---- ----
The return value may also have a `Timeout` value and/or the The returned tuple may also have a fourth element containing
atom `hibernate`. These options are useful for long living options for the sub protocol. No option is universal. While
connections. When they are not provided, the timeout value it will usually be a map of options, it doesn't have to be.
defaults to `infinity` and the hibernate value to `run`. For example loop handlers accept the atom `hibernate`.
The following snippet switches to the `my_protocol` sub The following snippet switches to the `my_protocol` sub
protocol, sets the timeout value to 5 seconds and enables protocol, sets the timeout value to 5 seconds and enables
hibernation: hibernation:
// @todo Yeah maybe what we really need is an Opts map.
[source,erlang] [source,erlang]
---- ----
init(Req, State) -> init(Req, State) ->
{my_protocol, Req, State, 5000, hibernate}. {my_protocol, Req, State, #{
timeout => 5000,
compress => true}}.
---- ----
If a sub protocol does not make use of these options, it should Sub protocols should ignore unknown options so as to not waste
crash if it receives anything other than the default values. resources doing unnecessary validation.
=== Upgrade === Upgrade
After the `init/2` function returns, Cowboy will then call the After the `init/2` function returns, Cowboy will call either
`upgrade/6` function. This is the only callback defined by the the `upgrade/4` or the `upgrade/5` function. The former is called
`cowboy_sub_protocol` behavior. when no options were given; the latter when they were given.
The function is named `upgrade` because it mimics the mechanism The function is named `upgrade` because it mimics the mechanism
of HTTP protocol upgrades. For some sub protocols, like Websocket, of HTTP protocol upgrades. For some sub protocols, like Websocket,
@ -53,16 +53,19 @@ only an upgrade at Cowboy's level and the client has nothing to
do about it. do about it.
The upgrade callback receives the Req object, the middleware The upgrade callback receives the Req object, the middleware
environment, the handler and its options, and the aforementioned environment, the handler and its state, and for `upgrade/5`
timeout and hibernate values. also the aformentioned options.
[source,erlang] [source,erlang]
---- ----
upgrade(Req, Env, Handler, HandlerOpts, Timeout, Hibernate) -> upgrade(Req, Env, Handler, State) ->
%% Sub protocol code here.
upgrade(Req, Env, Handler, State, Opts) ->
%% Sub protocol code here. %% Sub protocol code here.
---- ----
This callback is expected to behave like a middleware and to These callbacks are expected to behave like middlewares and to
return an updated environment and Req object. return an updated environment and Req object.
Sub protocols are expected to call the `cowboy_handler:terminate/4` Sub protocols are expected to call the `cowboy_handler:terminate/4`

View file

@ -60,13 +60,13 @@ be:
init(Req, State) -> init(Req, State) ->
case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of
undefined -> undefined ->
{ok, Req, State}; {cowboy_websocket, Req, State};
Subprotocols -> Subprotocols ->
case lists:keymember(<<"mqtt">>, 1, Subprotocols) of case lists:keymember(<<"mqtt">>, 1, Subprotocols) of
true -> true ->
Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>,
<<"mqtt">>, Req), <<"mqtt">>, Req),
{ok, Req2, State}; {cowboy_websocket, Req2, State};
false -> false ->
{stop, Req, State} {stop, Req, State}
end end
@ -210,12 +210,13 @@ than needed.
The `init/2` callback can set the timeout to be used The `init/2` callback can set the timeout to be used
for the connection. For example, this would make Cowboy for the connection. For example, this would make Cowboy
close connections idle for more than 60 seconds: close connections idle for more than 30 seconds:
[source,erlang] [source,erlang]
---- ----
init(Req, State) -> init(Req, State) ->
{cowboy_websocket, Req, State, 60000}. {cowboy_websocket, Req, State, #{
idle_timeout => 30000}}.
---- ----
This value cannot be changed once it is set. It defaults to This value cannot be changed once it is set. It defaults to

View file

@ -65,6 +65,5 @@ Cowboy's Websocket implementation also includes the
permessage-deflate and x-webkit-deflate-frame compression permessage-deflate and x-webkit-deflate-frame compression
extensions. extensions.
Cowboy will automatically use compression as long as the Cowboy will automatically use compression when the
`websocket_compress` protocol option is set when starting `compress` option is returned from the `init/2` function.
the listener.

View file

@ -59,8 +59,6 @@ Middlewares:
* ssl - Secure communication over sockets * ssl - Secure communication over sockets
* crypto - Crypto functions * crypto - Crypto functions
// @todo Explicitly depend on ssl.
All these applications must be started before the `cowboy` All these applications must be started before the `cowboy`
application. To start Cowboy and all dependencies at once: application. To start Cowboy and all dependencies at once:

View file

@ -29,8 +29,6 @@ Loop handlers implement the following interface:
init(Req, State) init(Req, State)
-> {cowboy_loop, Req, State} -> {cowboy_loop, Req, State}
| {cowboy_loop, Req, State, hibernate} | {cowboy_loop, Req, State, hibernate}
| {cowboy_loop, Req, State, timeout()}
| {cowboy_loop, Req, State, timeout(), hibernate}
info(Info, Req, State) info(Info, Req, State)
-> {ok, Req, State} -> {ok, Req, State}
@ -42,7 +40,7 @@ terminate(Reason, Req, State) -> ok %% optional
Req :: cowboy_req:req() Req :: cowboy_req:req()
State :: any() State :: any()
Info :: any() Info :: any()
Reason :: stop | timeout Reason :: stop
| {crash, error | exit | throw, any()} | {crash, error | exit | throw, any()}
---- ----
@ -65,37 +63,15 @@ stop::
The handler requested to close the connection by returning The handler requested to close the connection by returning
a `stop` tuple. a `stop` tuple.
timeout::
The connection has been closed due to inactivity. The timeout
value can be configured from `init/2`. The response sent when
this happens is a `204 No Content`.
{crash, Class, Reason}:: {crash, Class, Reason}::
A crash occurred in the handler. `Class` and `Reason` can be A crash occurred in the handler. `Class` and `Reason` can be
used to obtain more information about the crash. The function used to obtain more information about the crash. The function
`erlang:get_stacktrace/0` can also be called to obtain the `erlang:get_stacktrace/0` can also be called to obtain the
stacktrace of the process when the crash occurred. stacktrace of the process when the crash occurred.
//{error, overflow}::
// The connection is being closed and the process terminated
// because the buffer Cowboy uses to keep data sent by the
// client has reached its maximum. The buffer size can be
// configured through the environment value `loop_max_buffer`
// and defaults to 5000 bytes.
// +
// If the long running request comes with a body it is recommended
// to process this body before switching to the loop sub protocol.
//
//{error, closed}::
// The socket has been closed brutally without a close frame being
// received first.
//
//{error, Reason}::
// A socket error ocurred.
== Changelog == Changelog
* *2.0*: Cowboy temporarily no longer checks the socket for data with HTTP/1.1. * *2.0*: Loop handlers no longer need to handle overflow/timeouts.
* *1.0*: Behavior introduced. * *1.0*: Behavior introduced.
== See also == See also

View file

@ -21,9 +21,6 @@ REST handlers implement the following interface:
---- ----
init(Req, State) init(Req, State)
-> {cowboy_rest, Req, State} -> {cowboy_rest, Req, State}
| {cowboy_rest, Req, State, hibernate}
| {cowboy_rest, Req, State, timeout()}
| {cowboy_rest, Req, State, timeout(), hibernate}
Callback(Req, State) Callback(Req, State)
-> {Result, Req, State} -> {Result, Req, State}

View file

@ -10,31 +10,6 @@ The module `cowboy_websocket` implements Websocket
as a Ranch protocol. It also defines a callback interface as a Ranch protocol. It also defines a callback interface
for handling Websocket connections. for handling Websocket connections.
== Options
[source,erlang]
----
opts() :: #{
websocket_compress := boolean()
}
----
Configuration for the Websocket protocol.
This configuration is passed to Cowboy when starting listeners
using `cowboy:start_clear/4` or `cowboy:start_tls/4` functions.
It can be updated without restarting listeners using the
Ranch functions `ranch:get_protocol_options/1` and
`ranch:set_protocol_options/2`.
The default value is given next to the option name:
websocket_compress (false)::
Whether to enable the Websocket frame compression
extension. Frames will only be compressed for the
clients that support this extension.
== Callbacks == Callbacks
Websocket handlers must implement the following callback Websocket handlers must implement the following callback
@ -44,9 +19,7 @@ interface:
---- ----
init(Req, State) init(Req, State)
-> {cowboy_websocket, Req, State} -> {cowboy_websocket, Req, State}
| {cowboy_websocket, Req, State, hibernate} | {cowboy_websocket, Req, State, Opts}
| {cowboy_websocket, Req, State, timeout()}
| {cowboy_websocket, Req, State, timeout(), hibernate}
websocket_init(State) -> CallResult %% optional websocket_init(State) -> CallResult %% optional
websocket_handle(InFrame, State) -> CallResult websocket_handle(InFrame, State) -> CallResult
@ -56,6 +29,7 @@ terminate(Reason, undefined, State) -> ok %% optional
Req :: cowboy_req:req() Req :: cowboy_req:req()
State :: any() State :: any()
Opts :: cowboy_websocket:opts()
InFrame :: {text | binary | ping | pong, binary()} InFrame :: {text | binary | ping | pong, binary()}
OutFrame :: cow_ws:frame() OutFrame :: cow_ws:frame()
Info :: any() Info :: any()
@ -152,6 +126,42 @@ timeout::
{error, Reason}:: {error, Reason}::
A socket error ocurred. A socket error ocurred.
== Types
=== opts()
[source,erlang]
----
opts() :: #{
compress => boolean(),
idle_timeout => timeout()
}
----
Websocket handler options.
This configuration is passed to Cowboy from the `init/2`
function:
[source,erlang]
----
init(Req, State) ->
Opts = #{compress => true},
{cowboy_websocket, Req, State, Opts}.
----
The default value is given next to the option name:
compress (false)::
Whether to enable the Websocket frame compression
extension. Frames will only be compressed for the
clients that support this extension.
idle_timeout (60000)::
Time in milliseconds that Cowboy will keep the
connection open without receiving anything from
the client.
== Changelog == Changelog
* *2.0*: The Req object is no longer passed to Websocket callbacks. * *2.0*: The Req object is no longer passed to Websocket callbacks.

View file

@ -11,7 +11,7 @@ init(Req0, Opts) ->
<<"content-type">> => <<"text/event-stream">> <<"content-type">> => <<"text/event-stream">>
}, Req0), }, Req0),
erlang:send_after(1000, self(), {message, "Tick"}), erlang:send_after(1000, self(), {message, "Tick"}),
{cowboy_loop, Req, Opts, 5000}. {cowboy_loop, Req, Opts}.
info({message, Msg}, Req, State) -> info({message, Msg}, Req, State) ->
cowboy_req:stream_body(["id: ", id(), "\ndata: ", Msg, "\n\n"], nofin, Req), cowboy_req:stream_body(["id: ", id(), "\ndata: ", Msg, "\n\n"], nofin, Req),

View file

@ -20,7 +20,7 @@ define tpl_cowboy.loop
-export([info/3]). -export([info/3]).
init(Req, State) -> init(Req, State) ->
{cowboy_loop, Req, State, 5000, hibernate}. {cowboy_loop, Req, State, hibernate}.
info(_Info, Req, State) -> info(_Info, Req, State) ->
{ok, Req, State, hibernate}. {ok, Req, State, hibernate}.

View file

@ -25,9 +25,7 @@
-callback init(Req, any()) -callback init(Req, any())
-> {ok | module(), Req, any()} -> {ok | module(), Req, any()}
| {module(), Req, any(), hibernate} | {module(), Req, any(), any()}
| {module(), Req, any(), timeout()}
| {module(), Req, any(), timeout(), hibernate}
when Req::cowboy_req:req(). when Req::cowboy_req:req().
-callback terminate(any(), cowboy_req:req(), any()) -> ok. -callback terminate(any(), cowboy_req:req(), any()) -> ok.
@ -41,13 +39,9 @@ execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) ->
Result = terminate(normal, Req2, State, Handler), Result = terminate(normal, Req2, State, Handler),
{ok, Req2, Env#{result => Result}}; {ok, Req2, Env#{result => Result}};
{Mod, Req2, State} -> {Mod, Req2, State} ->
Mod:upgrade(Req2, Env, Handler, State, infinity, run); Mod:upgrade(Req2, Env, Handler, State);
{Mod, Req2, State, hibernate} -> {Mod, Req2, State, Opts} ->
Mod:upgrade(Req2, Env, Handler, State, infinity, hibernate); Mod:upgrade(Req2, Env, Handler, State, Opts)
{Mod, Req2, State, Timeout} ->
Mod:upgrade(Req2, Env, Handler, State, Timeout, run);
{Mod, Req2, State, Timeout, hibernate} ->
Mod:upgrade(Req2, Env, Handler, State, Timeout, hibernate)
catch Class:Reason -> catch Class:Reason ->
terminate({crash, Class, Reason}, Req, HandlerOpts, Handler), terminate({crash, Class, Reason}, Req, HandlerOpts, Handler),
erlang:raise(Class, Reason, erlang:get_stacktrace()) erlang:raise(Class, Reason, erlang:get_stacktrace())

View file

@ -136,11 +136,10 @@ init(Parent, Ref, Socket, Transport, Opts) ->
%% Timeouts: %% Timeouts:
%% - waiting for new request (if no stream is currently running) %% - waiting for new request (if no stream is currently running)
%% -> request_timeout: for whole request/headers, set at init/when we set ps_request_line{} state %% -> request_timeout: for whole request/headers, set at init/when we set ps_request_line{} state
%% - waiting for body (if a stream requested the body to be read) %% - waiting for new request, or body (when a stream is currently running)
%% -> read_body_timeout: amount of time we wait without receiving any data when reading the body %% -> idle_timeout: amount of time we wait without receiving any data
%% - if we skip the body, skip only for a specific duration %% - if we skip the body, skip only for a specific duration
%% -> skip_body_timeout: also have a skip_body_length %% -> skip_body_timeout: also have a skip_body_length
%% - none if we have a stream running and it didn't request the body to be read
%% - global %% - global
%% -> inactivity_timeout: max time to wait without anything happening before giving up %% -> inactivity_timeout: max time to wait without anything happening before giving up

View file

@ -12,27 +12,18 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% When using loop handlers, we are receiving data from the socket because we
%% want to know when the socket gets closed. This is generally not an issue
%% because these kinds of requests are generally not pipelined, and don't have
%% a body. If they do have a body, this body is often read in the
%% <em>init/2</em> callback and this is no problem. Otherwise, this data
%% accumulates in a buffer until we reach a certain threshold of 5000 bytes
%% by default. This can be configured through the <em>loop_max_buffer</em>
%% environment value. The request will be terminated with an
%% <em>{error, overflow}</em> reason if this threshold is reached.
-module(cowboy_loop). -module(cowboy_loop).
-behaviour(cowboy_sub_protocol). -behaviour(cowboy_sub_protocol).
-export([upgrade/6]). -export([upgrade/4]).
-export([upgrade/5]).
-export([loop/4]). -export([loop/4]).
-callback init(Req, any()) -callback init(Req, any())
-> {ok | module(), Req, any()} -> {ok | module(), Req, any()}
| {module(), Req, any(), hibernate} | {module(), Req, any(), any()}
| {module(), Req, any(), timeout()}
| {module(), Req, any(), timeout(), hibernate}
when Req::cowboy_req:req(). when Req::cowboy_req:req().
-callback info(any(), Req, State) -callback info(any(), Req, State)
-> {ok, Req, State} -> {ok, Req, State}
| {ok, Req, State, hibernate} | {ok, Req, State, hibernate}
@ -42,97 +33,44 @@
-callback terminate(any(), cowboy_req:req(), any()) -> ok. -callback terminate(any(), cowboy_req:req(), any()) -> ok.
-optional_callbacks([terminate/3]). -optional_callbacks([terminate/3]).
-record(state, { -spec upgrade(Req, Env, module(), any())
env :: cowboy_middleware:env(), -> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
hibernate = false :: boolean(),
buffer_size = 0 :: non_neg_integer(),
max_buffer = 5000 :: non_neg_integer() | infinity,
timeout = infinity :: timeout(),
timeout_ref = undefined :: undefined | reference()
}).
-spec upgrade(Req, Env, module(), any(), timeout(), run | hibernate)
-> {ok, Req, Env} | {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req(), Env::cowboy_middleware:env(). when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState, Timeout, Hibernate) -> upgrade(Req, Env, Handler, HandlerState) ->
State = #state{env=Env, max_buffer=get_max_buffer(Env), timeout=Timeout, loop(Req, Env, Handler, HandlerState).
hibernate=Hibernate =:= hibernate},
State2 = timeout(State),
before_loop(Req, State2, Handler, HandlerState).
get_max_buffer(#{loop_max_buffer := MaxBuffer}) -> MaxBuffer; -spec upgrade(Req, Env, module(), any(), hibernate)
get_max_buffer(_) -> 5000. -> {suspend, ?MODULE, loop, [any()]}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState, hibernate) ->
suspend(Req, Env, Handler, HandlerState).
before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) -> -spec loop(Req, Env, module(), any())
-> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
%% @todo Yeah we can't get the socket anymore. when Req::cowboy_req:req(), Env::cowboy_middleware:env().
%% Everything changes since we are a separate process now. %% @todo Handle system messages.
%% Proper flow control at the connection level should be implemented loop(Req, Env, Handler, HandlerState) ->
%% instead of what we have here.
% [Socket, Transport] = cowboy_req:get([socket, transport], Req),
% Transport:setopts(Socket, [{active, once}]),
{suspend, ?MODULE, loop, [Req, State#state{hibernate=false}, Handler, HandlerState]};
before_loop(Req, State, Handler, HandlerState) ->
%% Same here.
% [Socket, Transport] = cowboy_req:get([socket, transport], Req),
% Transport:setopts(Socket, [{active, once}]),
loop(Req, State, Handler, HandlerState).
%% Almost the same code can be found in cowboy_websocket.
timeout(State=#state{timeout=infinity}) ->
State#state{timeout_ref=undefined};
timeout(State=#state{timeout=Timeout,
timeout_ref=PrevRef}) ->
_ = case PrevRef of
undefined -> ignore%;
% @todo PrevRef -> erlang:cancel_timer(PrevRef)
end,
TRef = erlang:start_timer(Timeout, self(), ?MODULE),
State#state{timeout_ref=TRef}.
-spec loop(Req, #state{}, module(), any())
-> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req().
loop(Req, State=#state{timeout_ref=TRef}, Handler, HandlerState) ->
receive receive
{timeout, TRef, ?MODULE} ->
terminate(Req, State, Handler, HandlerState, timeout);
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
loop(Req, State, Handler, HandlerState);
Message -> Message ->
call(Req, State, Handler, HandlerState, Message) call(Req, Env, Handler, HandlerState, Message)
end. end.
call(Req, State, Handler, HandlerState, Message) -> call(Req0, Env, Handler, HandlerState0, Message) ->
try Handler:info(Message, Req, HandlerState) of try Handler:info(Message, Req0, HandlerState0) of
{ok, Req2, HandlerState2} -> {ok, Req, HandlerState} ->
before_loop(Req2, State, Handler, HandlerState2); loop(Req, Env, Handler, HandlerState);
{ok, Req2, HandlerState2, hibernate} -> {ok, Req, HandlerState, hibernate} ->
before_loop(Req2, State#state{hibernate=true}, Handler, HandlerState2); suspend(Req, Env, Handler, HandlerState);
{stop, Req2, HandlerState2} -> {stop, Req, HandlerState} ->
terminate(Req2, State, Handler, HandlerState2, stop) terminate(Req, Env, Handler, HandlerState, stop)
catch Class:Reason -> catch Class:Reason ->
cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler), cowboy_handler:terminate({crash, Class, Reason}, Req0, HandlerState0, Handler),
erlang:raise(Class, Reason, erlang:get_stacktrace()) erlang:raise(Class, Reason, erlang:get_stacktrace())
end. end.
terminate(Req, #state{env=Env, timeout_ref=TRef}, suspend(Req, Env, Handler, HandlerState) ->
Handler, HandlerState, Reason) -> {suspend, ?MODULE, loop, [Req, Env, Handler, HandlerState]}.
_ = case TRef of
undefined -> ignore; terminate(Req, Env, Handler, HandlerState, Reason) ->
TRef -> erlang:cancel_timer(TRef)
end,
flush_timeouts(),
Result = cowboy_handler:terminate(Reason, Req, HandlerState, Handler), Result = cowboy_handler:terminate(Reason, Req, HandlerState, Handler),
{ok, Req, Env#{result => Result}}. {ok, Req, Env#{result => Result}}.
flush_timeouts() ->
receive
{timeout, TRef, ?MODULE} when is_reference(TRef) ->
flush_timeouts()
after 0 ->
ok
end.

View file

@ -17,15 +17,14 @@
-module(cowboy_rest). -module(cowboy_rest).
-behaviour(cowboy_sub_protocol). -behaviour(cowboy_sub_protocol).
-export([upgrade/6]). -export([upgrade/4]).
-export([upgrade/5]).
%% Common handler callbacks. %% Common handler callbacks.
-callback init(Req, any()) -callback init(Req, any())
-> {ok | module(), Req, any()} -> {ok | module(), Req, any()}
| {module(), Req, any(), hibernate} | {module(), Req, any(), any()}
| {module(), Req, any(), timeout()}
| {module(), Req, any(), timeout(), hibernate}
when Req::cowboy_req:req(). when Req::cowboy_req:req().
-callback terminate(any(), cowboy_req:req(), any()) -> ok. -callback terminate(any(), cowboy_req:req(), any()) -> ok.
@ -232,14 +231,20 @@
expires :: undefined | no_call | calendar:datetime() | binary() expires :: undefined | no_call | calendar:datetime() | binary()
}). }).
-spec upgrade(Req, Env, module(), any(), infinity, run) -spec upgrade(Req, Env, module(), any())
-> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req0, Env, Handler, HandlerState, infinity, run) -> upgrade(Req0, Env, Handler, HandlerState) ->
Method = cowboy_req:method(Req0), Method = cowboy_req:method(Req0),
{ok, Req, Result} = service_available(Req0, #state{method=Method, {ok, Req, Result} = service_available(Req0, #state{method=Method,
handler=Handler, handler_state=HandlerState}), handler=Handler, handler_state=HandlerState}),
{ok, Req, Env#{result => Result}}. {ok, Req, Env#{result => Result}}.
-spec upgrade(Req, Env, module(), any(), any())
-> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env().
%% cowboy_rest takes no options.
upgrade(Req, Env, Handler, HandlerState, _Opts) ->
upgrade(Req, Env, Handler, HandlerState).
service_available(Req, State) -> service_available(Req, State) ->
expect(Req, State, service_available, true, fun known_methods/2, 503). expect(Req, State, service_available, true, fun known_methods/2, 503).

View file

@ -15,6 +15,10 @@
-module(cowboy_sub_protocol). -module(cowboy_sub_protocol).
-callback upgrade(Req, Env, module(), any(), timeout(), run | hibernate) -callback upgrade(Req, Env, module(), any())
-> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
-callback upgrade(Req, Env, module(), any(), any())
-> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req} -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req}
when Req::cowboy_req:req(), Env::cowboy_middleware:env(). when Req::cowboy_req:req(), Env::cowboy_middleware:env().

View file

@ -17,7 +17,8 @@
-module(cowboy_websocket). -module(cowboy_websocket).
-behaviour(cowboy_sub_protocol). -behaviour(cowboy_sub_protocol).
-export([upgrade/6]). -export([upgrade/4]).
-export([upgrade/5]).
-export([takeover/7]). -export([takeover/7]).
-export([handler_loop/3]). -export([handler_loop/3]).
@ -34,9 +35,7 @@
-callback init(Req, any()) -callback init(Req, any())
-> {ok | module(), Req, any()} -> {ok | module(), Req, any()}
| {module(), Req, any(), hibernate} | {module(), Req, any(), any()}
| {module(), Req, any(), timeout()}
| {module(), Req, any(), timeout(), hibernate}
when Req::cowboy_req:req(). when Req::cowboy_req:req().
-callback websocket_init(State) -callback websocket_init(State)
@ -53,6 +52,12 @@
-callback terminate(any(), cowboy_req:req(), any()) -> ok. -callback terminate(any(), cowboy_req:req(), any()) -> ok.
-optional_callbacks([terminate/3]). -optional_callbacks([terminate/3]).
-type opts() :: #{
idle_timeout => timeout(),
compress => boolean()
}.
-export_type([opts/0]).
-record(state, { -record(state, {
socket = undefined :: inet:socket() | undefined, socket = undefined :: inet:socket() | undefined,
transport = undefined :: module(), transport = undefined :: module(),
@ -60,6 +65,7 @@
key = undefined :: undefined | binary(), key = undefined :: undefined | binary(),
timeout = infinity :: timeout(), timeout = infinity :: timeout(),
timeout_ref = undefined :: undefined | reference(), timeout_ref = undefined :: undefined | reference(),
compress = false :: boolean(),
messages = undefined :: undefined | {atom(), atom(), atom()}, messages = undefined :: undefined | {atom(), atom(), atom()},
hibernate = false :: boolean(), hibernate = false :: boolean(),
frag_state = undefined :: cow_ws:frag_state(), frag_state = undefined :: cow_ws:frag_state(),
@ -70,14 +76,22 @@
%% Stream process. %% Stream process.
-spec upgrade(Req, Env, module(), any(), timeout(), run | hibernate) -spec upgrade(Req, Env, module(), any())
-> {ok, Req, Env}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState) ->
upgrade(Req, Env, Handler, HandlerState, #{}).
-spec upgrade(Req, Env, module(), any(), opts())
-> {ok, Req, Env} -> {ok, Req, Env}
when Req::cowboy_req:req(), Env::cowboy_middleware:env(). when Req::cowboy_req:req(), Env::cowboy_middleware:env().
%% @todo Immediately crash if a response has already been sent. %% @todo Immediately crash if a response has already been sent.
%% @todo Error out if HTTP/2. %% @todo Error out if HTTP/2.
upgrade(Req0, Env, Handler, HandlerState, Timeout, Hibernate) -> upgrade(Req0, Env, Handler, HandlerState, Opts) ->
try websocket_upgrade(#state{handler=Handler, timeout=Timeout, Timeout = maps:get(idle_timeout, Opts, 60000),
hibernate=Hibernate =:= hibernate}, Req0) of Compress = maps:get(compress, Opts, false),
State0 = #state{handler=Handler, timeout=Timeout, compress=Compress},
try websocket_upgrade(State0, Req0) of
{ok, State, Req} -> {ok, State, Req} ->
websocket_handshake(State, Req, HandlerState, Env) websocket_handshake(State, Req, HandlerState, Env)
catch _:_ -> catch _:_ ->
@ -104,14 +118,13 @@ websocket_upgrade(State, Req) ->
-spec websocket_extensions(#state{}, Req) -spec websocket_extensions(#state{}, Req)
-> {ok, #state{}, Req} when Req::cowboy_req:req(). -> {ok, #state{}, Req} when Req::cowboy_req:req().
websocket_extensions(State, Req=#{ref := Ref}) -> websocket_extensions(State=#state{compress=Compress}, Req) ->
%% @todo We want different options for this. For example %% @todo We want different options for this. For example
%% * compress everything auto %% * compress everything auto
%% * compress only text auto %% * compress only text auto
%% * compress only binary auto %% * compress only binary auto
%% * compress nothing auto (but still enabled it) %% * compress nothing auto (but still enabled it)
%% * disable compression %% * disable compression
Compress = maps:get(websocket_compress, ranch:get_protocol_options(Ref), false),
case {Compress, cowboy_req:parse_header(<<"sec-websocket-extensions">>, Req)} of case {Compress, cowboy_req:parse_header(<<"sec-websocket-extensions">>, Req)} of
{true, Extensions} when Extensions =/= undefined -> {true, Extensions} when Extensions =/= undefined ->
websocket_extensions(State, Req, Extensions, []); websocket_extensions(State, Req, Extensions, []);
@ -170,6 +183,7 @@ websocket_handshake(State=#state{key=Key},
{#state{}, any()}) -> ok. {#state{}, any()}) -> ok.
takeover(_Parent, Ref, Socket, Transport, _Opts, Buffer, takeover(_Parent, Ref, Socket, Transport, _Opts, Buffer,
{State0=#state{handler=Handler}, HandlerState}) -> {State0=#state{handler=Handler}, HandlerState}) ->
%% @todo We should have an option to disable this behavior.
ranch:remove_connection(Ref), ranch:remove_connection(Ref),
State1 = handler_loop_timeout(State0#state{socket=Socket, transport=Transport}), State1 = handler_loop_timeout(State0#state{socket=Socket, transport=Transport}),
State = State1#state{key=undefined, messages=Transport:messages()}, State = State1#state{key=undefined, messages=Transport:messages()},

View file

@ -11,7 +11,7 @@
init(Req, _) -> init(Req, _) ->
erlang:send_after(200, self(), timeout), erlang:send_after(200, self(), timeout),
{cowboy_loop, Req, 2, 5000, hibernate}. {cowboy_loop, Req, 2, hibernate}.
info(timeout, Req, 0) -> info(timeout, Req, 0) ->
%% @todo Why 102? %% @todo Why 102?

View file

@ -11,7 +11,7 @@
init(Req, _) -> init(Req, _) ->
self() ! timeout, self() ! timeout,
{cowboy_loop, Req, undefined, 5000, hibernate}. {cowboy_loop, Req, undefined, hibernate}.
info(timeout, Req0, State) -> info(timeout, Req0, State) ->
{ok, Body, Req} = cowboy_req:read_body(Req0), {ok, Body, Req} = cowboy_req:read_body(Req0),

View file

@ -12,7 +12,7 @@
init(Req, _) -> init(Req, _) ->
erlang:send_after(1000, self(), timeout), erlang:send_after(1000, self(), timeout),
{cowboy_loop, Req, undefined, 200, hibernate}. {cowboy_loop, Req, undefined, hibernate}.
info(timeout, Req, State) -> info(timeout, Req, State) ->
{stop, cowboy_req:reply(500, Req), State}. {stop, cowboy_req:reply(500, Req), State}.

View file

@ -9,7 +9,7 @@
init(Req, _) -> init(Req, _) ->
receive after 100 -> ok end, receive after 100 -> ok end,
self() ! stream, self() ! stream,
{cowboy_loop, Req, undefined, 100}. {cowboy_loop, Req, undefined}.
info(stream, Req, undefined) -> info(stream, Req, undefined) ->
stream(Req, 1, <<>>). stream(Req, 1, <<>>).

View file

@ -39,15 +39,13 @@ init_per_group(Name = autobahn, Config) ->
{skip, "Autobahn Test Suite not installed."}; {skip, "Autobahn Test Suite not installed."};
_ -> _ ->
{ok, _} = cowboy:start_clear(Name, 100, [{port, 33080}], #{ {ok, _} = cowboy:start_clear(Name, 100, [{port, 33080}], #{
env => #{dispatch => init_dispatch()}, env => #{dispatch => init_dispatch()}
websocket_compress => true
}), }),
Config Config
end; end;
init_per_group(Name = ws, Config) -> init_per_group(Name = ws, Config) ->
cowboy_test:init_http(Name, #{ cowboy_test:init_http(Name, #{
env => #{dispatch => init_dispatch()}, env => #{dispatch => init_dispatch()}
websocket_compress => true
}, Config). }, Config).
end_per_group(Listener, _Config) -> end_per_group(Listener, _Config) ->

View file

@ -7,7 +7,9 @@
-export([websocket_info/2]). -export([websocket_info/2]).
init(Req, _) -> init(Req, _) ->
{cowboy_websocket, Req, undefined}. {cowboy_websocket, Req, undefined, #{
compress => true
}}.
websocket_handle({text, Data}, State) -> websocket_handle({text, Data}, State) ->
{reply, {text, Data}, State}; {reply, {text, Data}, State};

View file

@ -9,7 +9,9 @@
init(Req, Opts) -> init(Req, Opts) ->
[Protocol | _] = cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req), [Protocol | _] = cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req),
Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, Protocol, Req), Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, Protocol, Req),
{cowboy_websocket, Req2, Opts, 1000}. {cowboy_websocket, Req2, Opts, #{
idle_timeout => 1000
}}.
websocket_handle(_Frame, State) -> websocket_handle(_Frame, State) ->
{ok, State}. {ok, State}.

View file

@ -8,7 +8,9 @@
init(Req, _) -> init(Req, _) ->
erlang:start_timer(500, self(), should_not_cancel_timer), erlang:start_timer(500, self(), should_not_cancel_timer),
{cowboy_websocket, Req, undefined, 1000}. {cowboy_websocket, Req, undefined, #{
idle_timeout => 1000
}}.
websocket_handle({text, Data}, State) -> websocket_handle({text, Data}, State) ->
{reply, {text, Data}, State}; {reply, {text, Data}, State};

View file

@ -3,11 +3,17 @@
-module(ws_timeout_hibernate). -module(ws_timeout_hibernate).
-export([init/2]). -export([init/2]).
-export([websocket_init/1]).
-export([websocket_handle/2]). -export([websocket_handle/2]).
-export([websocket_info/2]). -export([websocket_info/2]).
init(Req, _) -> init(Req, _) ->
{cowboy_websocket, Req, undefined, 1000, hibernate}. {cowboy_websocket, Req, undefined, #{
idle_timeout => 1000
}}.
websocket_init(State) ->
{ok, State, hibernate}.
websocket_handle(_Frame, State) -> websocket_handle(_Frame, State) ->
{ok, State, hibernate}. {ok, State, hibernate}.