From 0dc063ab7d94edb37c61f821b5d8e4c2da7f8ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 30 Sep 2014 20:12:13 +0300 Subject: Improve handler interface and documentation This change simplifies a little more the sub protocols mechanism. Aliases have been removed. The renaming of loop handlers as long polling handlers has been reverted. Plain HTTP handlers now simply do their work in the init/2 callback. There is no specific code for them. Loop handlers now follow the same return value as Websocket, they use ok to continue and shutdown to stop. Terminate reasons for all handler types have been documented. The terminate callback is now appropriately called in all cases (or should be). Behaviors for all handler types have been moved in the module that implement them. This means that cowboy_handler replaces the cowboy_http_handler behavior, and similarly cowboy_loop replaces cowboy_loop_handler, cowboy_websocket replaces cowboy_websocket_handler. Finally cowboy_rest now has the start of a behavior in it and will have the full list of optional callbacks defined once Erlang 18.0 gets released. The guide has been reorganized and should be easier to follow. --- src/cowboy_handler.erl | 62 ++++-------- src/cowboy_http_handler.erl | 36 ------- src/cowboy_long_polling.erl | 191 ------------------------------------ src/cowboy_loop.erl | 205 +++++++++++++++++++++++++++++++++++++++ src/cowboy_loop_handler.erl | 39 -------- src/cowboy_rest.erl | 16 ++- src/cowboy_static.erl | 6 +- src/cowboy_websocket.erl | 58 +++++++---- src/cowboy_websocket_handler.erl | 51 ---------- 9 files changed, 281 insertions(+), 383 deletions(-) delete mode 100644 src/cowboy_http_handler.erl delete mode 100644 src/cowboy_long_polling.erl create mode 100644 src/cowboy_loop.erl delete mode 100644 src/cowboy_loop_handler.erl delete mode 100644 src/cowboy_websocket_handler.erl (limited to 'src') diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl index a666714..e3faf66 100644 --- a/src/cowboy_handler.erl +++ b/src/cowboy_handler.erl @@ -21,7 +21,15 @@ -behaviour(cowboy_middleware). -export([execute/2]). --export([terminate/5]). +-export([terminate/4]). + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), hibernate} + | {module(), Req, any(), timeout()} + | {module(), Req, any(), timeout(), hibernate} + when Req::cowboy_req:req(). +%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. -spec execute(Req, Env) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). @@ -29,21 +37,21 @@ execute(Req, Env) -> {_, Handler} = lists:keyfind(handler, 1, Env), {_, HandlerOpts} = lists:keyfind(handler_opts, 1, Env), try Handler:init(Req, HandlerOpts) of - {http, Req2, State} -> - handle(Req2, Env, Handler, State); - {shutdown, Req2, State} -> - terminate(Req2, Env, Handler, State, {normal, shutdown}); + {ok, Req2, State} -> + Result = terminate(normal, Req2, State, Handler), + {ok, Req2, [{result, Result}|Env]}; {Mod, Req2, State} -> - upgrade(Req2, Env, Handler, State, infinity, run, Mod); + Mod:upgrade(Req2, Env, Handler, State, infinity, run); {Mod, Req2, State, hibernate} -> - upgrade(Req2, Env, Handler, State, infinity, hibernate, Mod); + Mod:upgrade(Req2, Env, Handler, State, infinity, hibernate); {Mod, Req2, State, Timeout} -> - upgrade(Req2, Env, Handler, State, Timeout, run, Mod); + Mod:upgrade(Req2, Env, Handler, State, Timeout, run); {Mod, Req2, State, Timeout, hibernate} -> - upgrade(Req2, Env, Handler, State, Timeout, hibernate, Mod) + Mod:upgrade(Req2, Env, Handler, State, Timeout, hibernate) catch Class:Reason -> Stacktrace = erlang:get_stacktrace(), cowboy_req:maybe_reply(Stacktrace, Req), + terminate({crash, Class, Reason}, Req, HandlerOpts, Handler), erlang:Class([ {reason, Reason}, {mfa, {Handler, init, 2}}, @@ -53,36 +61,9 @@ execute(Req, Env) -> ]) end. -handle(Req, Env, Handler, State) -> - try Handler:handle(Req, State) of - {ok, Req2, State2} -> - terminate(Req2, Env, Handler, State2, {normal, shutdown}) - catch Class:Reason -> - Stacktrace = erlang:get_stacktrace(), - cowboy_req:maybe_reply(Stacktrace, Req), - _ = terminate(Req, Env, Handler, State, Reason), - erlang:Class([ - {reason, Reason}, - {mfa, {Handler, handle, 2}}, - {stacktrace, Stacktrace}, - {req, cowboy_req:to_list(Req)}, - {state, State} - ]) - end. - -upgrade(Req, Env, Handler, State, Timeout, Hibernate, Mod) -> - Mod2 = case Mod of - long_polling -> cowboy_long_polling; - rest -> cowboy_rest; - ws -> cowboy_websocket; - _ when Mod =/= http -> Mod - end, - Mod2:upgrade(Req, Env, Handler, State, Timeout, Hibernate). - --spec terminate(Req, Env, module(), any(), {normal, shutdown} | {error, atom()} | any()) - -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). -terminate(Req, Env, Handler, State, Reason) -> - Result = case erlang:function_exported(Handler, terminate, 3) of +-spec terminate(any(), Req, any(), module()) -> ok when Req::cowboy_req:req(). +terminate(Reason, Req, State, Handler) -> + case erlang:function_exported(Handler, terminate, 3) of true -> try Handler:terminate(Reason, cowboy_req:lock(Req), State) @@ -98,5 +79,4 @@ terminate(Req, Env, Handler, State, Reason) -> end; false -> ok - end, - {ok, Req, [{result, Result}|Env]}. + end. diff --git a/src/cowboy_http_handler.erl b/src/cowboy_http_handler.erl deleted file mode 100644 index 75b41d2..0000000 --- a/src/cowboy_http_handler.erl +++ /dev/null @@ -1,36 +0,0 @@ -%% Copyright (c) 2011-2014, Loïc Hoguin -%% -%% 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(cowboy_http_handler). - --type opts() :: any(). --type state() :: any(). -%% @todo see terminate -%-type terminate_reason() :: {normal, shutdown} -% | {normal, timeout} %% Only occurs in loop handlers. -% | {error, closed} %% Only occurs in loop handlers. -% | {error, overflow} %% Only occurs in loop handlers. -% | {error, atom()}. - --callback init(Req, opts()) - -> {http, Req, state()} - | {long_polling | rest | ws | module(), Req, state()} - | {long_polling | rest | ws | module(), Req, state(), hibernate} - | {long_polling | rest | ws | module(), Req, state(), timeout()} - | {long_polling | rest | ws | module(), Req, state(), timeout(), hibernate} - | {shutdown, Req, state()} - when Req::cowboy_req:req(). --callback handle(Req, State) -> {ok, Req, State} - when Req::cowboy_req:req(), State::state(). -%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. diff --git a/src/cowboy_long_polling.erl b/src/cowboy_long_polling.erl deleted file mode 100644 index 6616a78..0000000 --- a/src/cowboy_long_polling.erl +++ /dev/null @@ -1,191 +0,0 @@ -%% Copyright (c) 2011-2014, Loïc Hoguin -%% -%% 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. - -%% 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 -%% init/2 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 loop_max_buffer -%% environment value. The request will be terminated with an -%% {error, overflow} reason if this threshold is reached. --module(cowboy_long_polling). --behaviour(cowboy_sub_protocol). - --export([upgrade/6]). --export([loop/4]). - --record(state, { - env :: cowboy_middleware:env(), - hibernate = false :: boolean(), - buffer_size = 0 :: non_neg_integer(), - max_buffer = 5000 :: non_neg_integer() | infinity, - timeout = infinity :: timeout(), - timeout_ref = undefined :: undefined | reference(), - resp_sent = false :: boolean() -}). - --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(). -upgrade(Req, Env, Handler, HandlerState, Timeout, run) -> - State = #state{env=Env, max_buffer=get_max_buffer(Env), timeout=Timeout}, - State2 = timeout(State), - after_call(Req, State2, Handler, HandlerState); -upgrade(Req, Env, Handler, HandlerState, Timeout, hibernate) -> - State = #state{env=Env, max_buffer=get_max_buffer(Env), hibernate=true, timeout=Timeout}, - State2 = timeout(State), - after_call(Req, State2, Handler, HandlerState). - -get_max_buffer(Env) -> - case lists:keyfind(loop_max_buffer, 1, Env) of - false -> 5000; - {_, MaxBuffer} -> MaxBuffer - end. - -%% Update the state if the response was sent in the callback. -after_call(Req, State=#state{resp_sent=false}, Handler, - HandlerState) -> - receive - {cowboy_req, resp_sent} -> - before_loop(Req, State#state{resp_sent=true}, Handler, HandlerState) - after 0 -> - before_loop(Req, State, Handler, HandlerState) - end; -after_call(Req, State, Handler, HandlerState) -> - before_loop(Req, State, Handler, HandlerState). - -before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) -> - [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) -> - [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; - 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{env=Env, buffer_size=NbBytes, - max_buffer=Threshold, timeout_ref=TRef, - resp_sent=RespSent}, Handler, HandlerState) -> - [Socket, Transport] = cowboy_req:get([socket, transport], Req), - {OK, Closed, Error} = Transport:messages(), - receive - {OK, Socket, Data} -> - NbBytes2 = NbBytes + byte_size(Data), - if NbBytes2 > Threshold -> - _ = if RespSent -> ok; true -> - cowboy_req:reply(500, Req) - end, - _ = cowboy_handler:terminate(Req, Env, Handler, HandlerState, {error, overflow}), - exit(normal); - true -> - Req2 = cowboy_req:append_buffer(Data, Req), - State2 = timeout(State#state{buffer_size=NbBytes2}), - before_loop(Req2, State2, Handler, HandlerState) - end; - {Closed, Socket} -> - terminate(Req, State, Handler, HandlerState, {error, closed}); - {Error, Socket, Reason} -> - terminate(Req, State, Handler, HandlerState, {error, Reason}); - {timeout, TRef, ?MODULE} -> - after_loop(Req, State, Handler, HandlerState, {normal, timeout}); - {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> - loop(Req, State, Handler, HandlerState); - Message -> - %% We set the socket back to {active, false} mode in case - %% the handler is going to call recv. We also flush any - %% data received after that and put it into the buffer. - %% We do not check the size here, if data keeps coming - %% we'll error out on the next packet received. - Transport:setopts(Socket, [{active, false}]), - Req2 = receive {OK, Socket, Data} -> - cowboy_req:append_buffer(Data, Req) - after 0 -> - Req - end, - call(Req2, State, Handler, HandlerState, Message) - end. - -call(Req, State=#state{env=Env, resp_sent=RespSent}, - Handler, HandlerState, Message) -> - try Handler:info(Message, Req, HandlerState) of - {ok, Req2, HandlerState2} -> - after_loop(Req2, State, Handler, HandlerState2, {normal, shutdown}); - {loop, Req2, HandlerState2} -> - after_call(Req2, State, Handler, HandlerState2); - {loop, Req2, HandlerState2, hibernate} -> - after_call(Req2, State#state{hibernate=true}, Handler, HandlerState2) - catch Class:Reason -> - Stacktrace = erlang:get_stacktrace(), - if RespSent -> ok; true -> - cowboy_req:maybe_reply(Stacktrace, Req) - end, - _ = cowboy_handler:terminate(Req, Env, Handler, HandlerState, {error, overflow}), - erlang:Class([ - {reason, Reason}, - {mfa, {Handler, info, 3}}, - {stacktrace, Stacktrace}, - {req, cowboy_req:to_list(Req)}, - {state, HandlerState} - ]) - end. - -%% It is sometimes important to make a socket passive as it was initially -%% and as it is expected to be by cowboy_protocol, right after we're done -%% with loop handling. The browser may freely pipeline a bunch of requests -%% if previous one was, say, a JSONP long-polling request. -after_loop(Req, State, Handler, HandlerState, Reason) -> - [Socket, Transport] = cowboy_req:get([socket, transport], Req), - Transport:setopts(Socket, [{active, false}]), - {OK, _Closed, _Error} = Transport:messages(), - Req2 = receive - {OK, Socket, Data} -> - cowboy_req:append_buffer(Data, Req) - after 0 -> - Req - end, - terminate(Req2, State, Handler, HandlerState, Reason). - -terminate(Req, #state{env=Env, timeout_ref=TRef}, - Handler, HandlerState, Reason) -> - _ = case TRef of - undefined -> ignore; - TRef -> erlang:cancel_timer(TRef) - end, - flush_timeouts(), - cowboy_handler:terminate(Req, Env, Handler, HandlerState, Reason). - -flush_timeouts() -> - receive - {timeout, TRef, ?MODULE} when is_reference(TRef) -> - flush_timeouts() - after 0 -> - ok - end. diff --git a/src/cowboy_loop.erl b/src/cowboy_loop.erl new file mode 100644 index 0000000..b9eb8cd --- /dev/null +++ b/src/cowboy_loop.erl @@ -0,0 +1,205 @@ +%% Copyright (c) 2011-2014, Loïc Hoguin +%% +%% 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. + +%% 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 +%% init/2 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 loop_max_buffer +%% environment value. The request will be terminated with an +%% {error, overflow} reason if this threshold is reached. +-module(cowboy_loop). +-behaviour(cowboy_sub_protocol). + +-export([upgrade/6]). +-export([loop/4]). + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), hibernate} + | {module(), Req, any(), timeout()} + | {module(), Req, any(), timeout(), hibernate} + when Req::cowboy_req:req(). +-callback info(any(), Req, State) + -> {ok, Req, State} + | {ok, Req, State, hibernate} + | {shutdown, Req, State} + when Req::cowboy_req:req(), State::any(). +%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. + +-record(state, { + env :: cowboy_middleware:env(), + hibernate = false :: boolean(), + buffer_size = 0 :: non_neg_integer(), + max_buffer = 5000 :: non_neg_integer() | infinity, + timeout = infinity :: timeout(), + timeout_ref = undefined :: undefined | reference(), + resp_sent = false :: boolean() +}). + +-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(). +upgrade(Req, Env, Handler, HandlerState, Timeout, run) -> + State = #state{env=Env, max_buffer=get_max_buffer(Env), timeout=Timeout}, + State2 = timeout(State), + after_call(Req, State2, Handler, HandlerState); +upgrade(Req, Env, Handler, HandlerState, Timeout, hibernate) -> + State = #state{env=Env, max_buffer=get_max_buffer(Env), hibernate=true, timeout=Timeout}, + State2 = timeout(State), + after_call(Req, State2, Handler, HandlerState). + +get_max_buffer(Env) -> + case lists:keyfind(loop_max_buffer, 1, Env) of + false -> 5000; + {_, MaxBuffer} -> MaxBuffer + end. + +%% Update the state if the response was sent in the callback. +after_call(Req, State=#state{resp_sent=false}, Handler, + HandlerState) -> + receive + {cowboy_req, resp_sent} -> + before_loop(Req, State#state{resp_sent=true}, Handler, HandlerState) + after 0 -> + before_loop(Req, State, Handler, HandlerState) + end; +after_call(Req, State, Handler, HandlerState) -> + before_loop(Req, State, Handler, HandlerState). + +before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) -> + [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) -> + [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; + 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{buffer_size=NbBytes, + max_buffer=Threshold, timeout_ref=TRef, + resp_sent=RespSent}, Handler, HandlerState) -> + [Socket, Transport] = cowboy_req:get([socket, transport], Req), + {OK, Closed, Error} = Transport:messages(), + receive + {OK, Socket, Data} -> + NbBytes2 = NbBytes + byte_size(Data), + if NbBytes2 > Threshold -> + _ = if RespSent -> ok; true -> + cowboy_req:reply(500, Req) + end, + cowboy_handler:terminate({error, overflow}, Req, HandlerState, Handler), + exit(normal); + true -> + Req2 = cowboy_req:append_buffer(Data, Req), + State2 = timeout(State#state{buffer_size=NbBytes2}), + before_loop(Req2, State2, Handler, HandlerState) + end; + {Closed, Socket} -> + terminate(Req, State, Handler, HandlerState, {error, closed}); + {Error, Socket, Reason} -> + terminate(Req, State, Handler, HandlerState, {error, Reason}); + {timeout, TRef, ?MODULE} -> + after_loop(Req, State, Handler, HandlerState, timeout); + {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> + loop(Req, State, Handler, HandlerState); + Message -> + %% We set the socket back to {active, false} mode in case + %% the handler is going to call recv. We also flush any + %% data received after that and put it into the buffer. + %% We do not check the size here, if data keeps coming + %% we'll error out on the next packet received. + Transport:setopts(Socket, [{active, false}]), + Req2 = receive {OK, Socket, Data} -> + cowboy_req:append_buffer(Data, Req) + after 0 -> + Req + end, + call(Req2, State, Handler, HandlerState, Message) + end. + +call(Req, State=#state{resp_sent=RespSent}, + Handler, HandlerState, Message) -> + try Handler:info(Message, Req, HandlerState) of + {ok, Req2, HandlerState2} -> + after_call(Req2, State, Handler, HandlerState2); + {ok, Req2, HandlerState2, hibernate} -> + after_call(Req2, State#state{hibernate=true}, Handler, HandlerState2); + {shutdown, Req2, HandlerState2} -> + after_loop(Req2, State, Handler, HandlerState2, shutdown) + catch Class:Reason -> + Stacktrace = erlang:get_stacktrace(), + if RespSent -> ok; true -> + cowboy_req:maybe_reply(Stacktrace, Req) + end, + cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler), + erlang:Class([ + {reason, Reason}, + {mfa, {Handler, info, 3}}, + {stacktrace, Stacktrace}, + {req, cowboy_req:to_list(Req)}, + {state, HandlerState} + ]) + end. + +%% It is sometimes important to make a socket passive as it was initially +%% and as it is expected to be by cowboy_protocol, right after we're done +%% with loop handling. The browser may freely pipeline a bunch of requests +%% if previous one was, say, a JSONP long-polling request. +after_loop(Req, State, Handler, HandlerState, Reason) -> + [Socket, Transport] = cowboy_req:get([socket, transport], Req), + Transport:setopts(Socket, [{active, false}]), + {OK, _Closed, _Error} = Transport:messages(), + Req2 = receive + {OK, Socket, Data} -> + cowboy_req:append_buffer(Data, Req) + after 0 -> + Req + end, + terminate(Req2, State, Handler, HandlerState, Reason). + +terminate(Req, #state{env=Env, timeout_ref=TRef}, + Handler, HandlerState, Reason) -> + _ = case TRef of + undefined -> ignore; + TRef -> erlang:cancel_timer(TRef) + end, + flush_timeouts(), + Result = cowboy_handler:terminate(Reason, Req, HandlerState, Handler), + {ok, Req, [{result, Result}|Env]}. + +flush_timeouts() -> + receive + {timeout, TRef, ?MODULE} when is_reference(TRef) -> + flush_timeouts() + after 0 -> + ok + end. diff --git a/src/cowboy_loop_handler.erl b/src/cowboy_loop_handler.erl deleted file mode 100644 index 5e50c34..0000000 --- a/src/cowboy_loop_handler.erl +++ /dev/null @@ -1,39 +0,0 @@ -%% Copyright (c) 2011-2014, Loïc Hoguin -%% -%% 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(cowboy_loop_handler). - --type opts() :: any(). --type state() :: any(). -%% @todo see terminate -%-type terminate_reason() :: {normal, shutdown} -% | {normal, timeout} -% | {error, closed} -% | {error, overflow} -% | {error, atom()}. - --callback init(Req, opts()) - -> {http, Req, state()} - | {long_polling | rest | ws | module(), Req, state()} - | {long_polling | rest | ws | module(), Req, state(), hibernate} - | {long_polling | rest | ws | module(), Req, state(), timeout()} - | {long_polling | rest | ws | module(), Req, state(), timeout(), hibernate} - | {shutdown, Req, state()} - when Req::cowboy_req:req(). --callback info(any(), Req, State) - -> {ok, Req, State} - | {loop, Req, State} - | {loop, Req, State, hibernate} - when Req::cowboy_req:req(), State::state(). -%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 63f33e6..98c6f3b 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -19,6 +19,15 @@ -export([upgrade/6]). +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), hibernate} + | {module(), Req, any(), timeout()} + | {module(), Req, any(), timeout(), hibernate} + when Req::cowboy_req:req(). +%% @todo optional REST callbacks +%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. + -record(state, { env :: cowboy_middleware:env(), method = undefined :: binary(), @@ -967,11 +976,11 @@ next(Req, State, StatusCode) when is_integer(StatusCode) -> respond(Req, State, StatusCode) -> terminate(cowboy_req:reply(StatusCode, Req), State). -error_terminate(Req, State=#state{handler=Handler, handler_state=HandlerState}, +error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, Class, Reason, Callback) -> - _ = terminate(Req, State), Stacktrace = erlang:get_stacktrace(), cowboy_req:maybe_reply(Stacktrace, Req), + cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler), erlang:Class([ {reason, Reason}, {mfa, {Handler, Callback, 2}}, @@ -981,4 +990,5 @@ error_terminate(Req, State=#state{handler=Handler, handler_state=HandlerState}, ]). terminate(Req, #state{env=Env, handler=Handler, handler_state=HandlerState}) -> - cowboy_handler:terminate(Req, Env, Handler, HandlerState, {normal, shutdown}). + Result = cowboy_handler:terminate(normal, Req, HandlerState, Handler), + {ok, Req, [{result, Result}|Env]}. diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index ecca6fa..99ad82e 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -42,7 +42,7 @@ %% If the handler is configured to manage a directory, check that the %% requested file is inside the configured directory. --spec init(Req, opts()) -> {rest, Req, error | state()} when Req::cowboy_req:req(). +-spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req(). init(Req, {Name, Path}) -> init_opts(Req, {Name, Path, []}); init(Req, {Name, App, Path}) @@ -87,7 +87,7 @@ init_dir(Req, Path, Extra) -> << Dir:Len/binary, $/, _/binary >> -> init_info(Req, Filepath, Extra); _ -> - {rest, Req, error} + {cowboy_rest, Req, error} end. fullpath(Path) -> @@ -105,7 +105,7 @@ fullpath([Segment|Tail], Acc) -> init_info(Req, Path, Extra) -> Info = file:read_file_info(Path, [{time, universal}]), - {rest, Req, {Path, Info, Extra}}. + {cowboy_rest, Req, {Path, Info, Extra}}. -ifdef(TEST). fullpath_test_() -> diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index e0b20ca..c700af9 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -33,8 +33,32 @@ -type frag_state() :: undefined | {nofin, opcode(), binary()} | {fin, opcode(), binary()}. -type rsv() :: << _:3 >>. --type terminate_reason() :: {normal | error | remote, atom()} - | {remote, close_code(), binary()}. +-type terminate_reason() :: normal | shutdown | timeout + | remote | {remote, close_code(), binary()} + | {error, badencoding | badframe | closed | atom()} + | {crash, error | exit | throw, any()}. + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), hibernate} + | {module(), Req, any(), timeout()} + | {module(), Req, any(), timeout(), hibernate} + when Req::cowboy_req:req(). +-callback websocket_handle({text | binary | ping | pong, binary()}, Req, State) + -> {ok, Req, State} + | {ok, Req, State, hibernate} + | {reply, frame() | [frame()], Req, State} + | {reply, frame() | [frame()], Req, State, hibernate} + | {shutdown, Req, State} + when Req::cowboy_req:req(), State::any(). +-callback websocket_info(any(), Req, State) + -> {ok, Req, State} + | {ok, Req, State, hibernate} + | {reply, frame() | [frame()], Req, State} + | {reply, frame() | [frame()], Req, State, hibernate} + | {shutdown, Req, State} + when Req::cowboy_req:req(), State::any(). +%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. -record(state, { env :: cowboy_middleware:env(), @@ -181,7 +205,7 @@ handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error}, {Error, Socket, Reason} -> handler_terminate(State, Req, HandlerState, {error, Reason}); {timeout, TRef, ?MODULE} -> - websocket_close(State, Req, HandlerState, {normal, timeout}); + websocket_close(State, Req, HandlerState, timeout); {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> handler_loop(State, Req, HandlerState, SoFar); Message -> @@ -487,7 +511,7 @@ websocket_payload_loop(State=#state{socket=Socket, transport=Transport, {Error, Socket, Reason} -> handler_terminate(State, Req, HandlerState, {error, Reason}); {timeout, TRef, ?MODULE} -> - websocket_close(State, Req, HandlerState, {normal, timeout}); + websocket_close(State, Req, HandlerState, timeout); {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> websocket_payload_loop(State, Req, HandlerState, Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv); @@ -524,7 +548,7 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 2, Payload) -> websocket_handle, {binary, Payload}, fun websocket_data/4); %% Close control frame. websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, <<>>) -> - websocket_close(State, Req, HandlerState, {remote, closed}); + websocket_close(State, Req, HandlerState, remote); websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, << Code:16, Payload/bits >>) -> websocket_close(State, Req, HandlerState, {remote, Code, Payload}); @@ -558,8 +582,7 @@ handler_call(State=#state{handler=Handler}, Req, HandlerState, {ok, State2} -> NextState(State2, Req2, HandlerState2, RemainingData); {shutdown, State2} -> - handler_terminate(State2, Req2, HandlerState2, - {normal, shutdown}); + handler_terminate(State2, Req2, HandlerState2, shutdown); {{error, _} = Error, State2} -> handler_terminate(State2, Req2, HandlerState2, Error) end; @@ -570,8 +593,7 @@ handler_call(State=#state{handler=Handler}, Req, HandlerState, NextState(State2#state{hibernate=true}, Req2, HandlerState2, RemainingData); {shutdown, State2} -> - handler_terminate(State2, Req2, HandlerState2, - {normal, shutdown}); + handler_terminate(State2, Req2, HandlerState2, shutdown); {{error, _} = Error, State2} -> handler_terminate(State2, Req2, HandlerState2, Error) end; @@ -580,8 +602,7 @@ handler_call(State=#state{handler=Handler}, Req, HandlerState, {ok, State2} -> NextState(State2, Req2, HandlerState2, RemainingData); {shutdown, State2} -> - handler_terminate(State2, Req2, HandlerState2, - {normal, shutdown}); + handler_terminate(State2, Req2, HandlerState2, shutdown); {{error, _} = Error, State2} -> handler_terminate(State2, Req2, HandlerState2, Error) end; @@ -591,15 +612,14 @@ handler_call(State=#state{handler=Handler}, Req, HandlerState, NextState(State2#state{hibernate=true}, Req2, HandlerState2, RemainingData); {shutdown, State2} -> - handler_terminate(State2, Req2, HandlerState2, - {normal, shutdown}); + handler_terminate(State2, Req2, HandlerState2, shutdown); {{error, _} = Error, State2} -> handler_terminate(State2, Req2, HandlerState2, Error) end; {shutdown, Req2, HandlerState2} -> - websocket_close(State, Req2, HandlerState2, {normal, shutdown}) + websocket_close(State, Req2, HandlerState2, shutdown) catch Class:Reason -> - _ = websocket_close(State, Req, HandlerState, {error, handler}), + _ = websocket_close(State, Req, HandlerState, {crash, Class, Reason}), erlang:Class([ {reason, Reason}, {mfa, {Handler, Callback, 3}}, @@ -696,15 +716,15 @@ websocket_send_many([Frame|Tail], State) -> websocket_close(State=#state{socket=Socket, transport=Transport}, Req, HandlerState, Reason) -> case Reason of - {normal, _} -> + Normal when Normal =:= shutdown; Normal =:= timeout -> Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>); {error, badframe} -> Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1002:16 >>); {error, badencoding} -> Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1007:16 >>); - {error, handler} -> + {crash, _, _} -> Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1011:16 >>); - {remote, closed} -> + remote -> Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>); {remote, Code, _} -> Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, Code:16 >>) @@ -716,5 +736,5 @@ websocket_close(State=#state{socket=Socket, transport=Transport}, when Req::cowboy_req:req(). handler_terminate(#state{env=Env, handler=Handler}, Req, HandlerState, Reason) -> - _ = cowboy_handler:terminate(Req, Env, Handler, HandlerState, Reason), + cowboy_handler:terminate(Reason, Req, HandlerState, Handler), {ok, Req, [{result, closed}|Env]}. diff --git a/src/cowboy_websocket_handler.erl b/src/cowboy_websocket_handler.erl deleted file mode 100644 index a6a036b..0000000 --- a/src/cowboy_websocket_handler.erl +++ /dev/null @@ -1,51 +0,0 @@ -%% Copyright (c) 2011-2014, Loïc Hoguin -%% -%% 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(cowboy_websocket_handler). - --type opts() :: any(). --type state() :: any(). -%% @todo see terminate -%-type terminate_reason() :: {normal, shutdown} -% | {normal, timeout} -% | {error, closed} -% | {remote, closed} -% | {remote, cowboy_websocket:close_code(), binary()} -% | {error, badencoding} -% | {error, badframe} -% | {error, atom()}. - --callback init(Req, opts()) - -> {http, Req, state()} - | {long_polling | rest | ws | module(), Req, state()} - | {long_polling | rest | ws | module(), Req, state(), hibernate} - | {long_polling | rest | ws | module(), Req, state(), timeout()} - | {long_polling | rest | ws | module(), Req, state(), timeout(), hibernate} - | {shutdown, Req, state()} - when Req::cowboy_req:req(). --callback websocket_handle({text | binary | ping | pong, binary()}, Req, State) - -> {ok, Req, State} - | {ok, Req, State, hibernate} - | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State} - | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State, hibernate} - | {shutdown, Req, State} - when Req::cowboy_req:req(), State::state(). --callback websocket_info(any(), Req, State) - -> {ok, Req, State} - | {ok, Req, State, hibernate} - | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State} - | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State, hibernate} - | {shutdown, Req, State} - when Req::cowboy_req:req(), State::state(). -%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. -- cgit v1.2.3