From 5ce4c2bfb40ecc4b687a2941e612025a1c4ff913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 26 Sep 2014 15:58:44 +0300 Subject: Unify the init and terminate callbacks This set of changes is the first step to simplify the writing of handlers, by removing some extraneous callbacks and making others optional. init/3 is now init/2, its first argument being removed. rest_init/2 and rest_terminate/2 have been removed. websocket_init/3 and websocket_terminate/3 have been removed. terminate/3 is now optional. It is called regardless of the type of handler, including rest and websocket. The return value of init/2 changed. It now returns {Mod, Req, Opts} with Mod being either one of the four handler type or a custom module. It can also return extra timeout and hibernate options. The signature for sub protocols has changed, they now receive these extra timeout and hibernate options. Loop handlers are now implemented in cowboy_long_polling, and will be renamed throughout the project in a future commit. --- src/cowboy_handler.erl | 299 +++++++-------------------------------- src/cowboy_http_handler.erl | 27 ++-- src/cowboy_long_polling.erl | 191 +++++++++++++++++++++++++ src/cowboy_loop_handler.erl | 27 ++-- src/cowboy_req.erl | 10 -- src/cowboy_rest.erl | 43 ++---- src/cowboy_static.erl | 55 ++++--- src/cowboy_sub_protocol.erl | 7 +- src/cowboy_websocket.erl | 80 +++-------- src/cowboy_websocket_handler.erl | 33 ++--- 10 files changed, 335 insertions(+), 437 deletions(-) create mode 100644 src/cowboy_long_polling.erl (limited to 'src') diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl index 1e8261f..a666714 100644 --- a/src/cowboy_handler.erl +++ b/src/cowboy_handler.erl @@ -17,287 +17,86 @@ %% Execute the handler given by the handler and handler_opts %% environment values. The result of this execution is added to the %% environment under the result value. -%% -%% 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/3 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_handler). -behaviour(cowboy_middleware). -export([execute/2]). --export([handler_loop/4]). - --record(state, { - env :: cowboy_middleware:env(), - hibernate = false :: boolean(), - loop_buffer_size = 0 :: non_neg_integer(), - loop_max_buffer = 5000 :: non_neg_integer() | infinity, - loop_timeout = infinity :: timeout(), - loop_timeout_ref = undefined :: undefined | reference(), - resp_sent = false :: boolean() -}). +-export([terminate/5]). --spec execute(Req, Env) - -> {ok, Req, Env} | {suspend, ?MODULE, handler_loop, [any()]} +-spec execute(Req, Env) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). execute(Req, Env) -> {_, Handler} = lists:keyfind(handler, 1, Env), {_, HandlerOpts} = lists:keyfind(handler_opts, 1, Env), - MaxBuffer = case lists:keyfind(loop_max_buffer, 1, Env) of - false -> 5000; - {_, MaxBuffer0} -> MaxBuffer0 - end, - handler_init(Req, #state{env=Env, loop_max_buffer=MaxBuffer}, - Handler, HandlerOpts). - --spec handler_init(Req, #state{}, module(), any()) - -> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]} - when Req::cowboy_req:req(). -handler_init(Req, State, Handler, HandlerOpts) -> - Transport = cowboy_req:get(transport, Req), - try Handler:init({Transport:name(), http}, Req, HandlerOpts) of - {ok, Req2, HandlerState} -> - handler_handle(Req2, State, Handler, HandlerState); - {loop, Req2, HandlerState} -> - handler_after_callback(Req2, State, Handler, HandlerState); - {loop, Req2, HandlerState, hibernate} -> - handler_after_callback(Req2, State#state{hibernate=true}, - Handler, HandlerState); - {loop, Req2, HandlerState, Timeout} -> - State2 = handler_loop_timeout(State#state{loop_timeout=Timeout}), - handler_after_callback(Req2, State2, Handler, HandlerState); - {loop, Req2, HandlerState, Timeout, hibernate} -> - State2 = handler_loop_timeout(State#state{ - hibernate=true, loop_timeout=Timeout}), - handler_after_callback(Req2, State2, Handler, HandlerState); - {shutdown, Req2, HandlerState} -> - terminate_request(Req2, State, Handler, HandlerState, - {normal, shutdown}); - {upgrade, protocol, Module} -> - upgrade_protocol(Req, State, Handler, HandlerOpts, Module); - {upgrade, protocol, Module, Req2, HandlerOpts2} -> - upgrade_protocol(Req2, State, Handler, HandlerOpts2, Module) + try Handler:init(Req, HandlerOpts) of + {http, Req2, State} -> + handle(Req2, Env, Handler, State); + {shutdown, Req2, State} -> + terminate(Req2, Env, Handler, State, {normal, shutdown}); + {Mod, Req2, State} -> + upgrade(Req2, Env, Handler, State, infinity, run, Mod); + {Mod, Req2, State, hibernate} -> + upgrade(Req2, Env, Handler, State, infinity, hibernate, Mod); + {Mod, Req2, State, Timeout} -> + upgrade(Req2, Env, Handler, State, Timeout, run, Mod); + {Mod, Req2, State, Timeout, hibernate} -> + upgrade(Req2, Env, Handler, State, Timeout, hibernate, Mod) catch Class:Reason -> Stacktrace = erlang:get_stacktrace(), cowboy_req:maybe_reply(Stacktrace, Req), erlang:Class([ {reason, Reason}, - {mfa, {Handler, init, 3}}, + {mfa, {Handler, init, 2}}, {stacktrace, Stacktrace}, {req, cowboy_req:to_list(Req)}, {opts, HandlerOpts} ]) end. --spec upgrade_protocol(Req, #state{}, module(), any(), module()) - -> {ok, Req, Env} - | {suspend, module(), atom(), any()} - | {halt, Req} - when Req::cowboy_req:req(), Env::cowboy_middleware:env(). -upgrade_protocol(Req, #state{env=Env}, - Handler, HandlerOpts, Module) -> - Module:upgrade(Req, Env, Handler, HandlerOpts). - --spec handler_handle(Req, #state{}, module(), any()) - -> {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). -handler_handle(Req, State, Handler, HandlerState) -> - try Handler:handle(Req, HandlerState) of - {ok, Req2, HandlerState2} -> - terminate_request(Req2, State, Handler, HandlerState2, - {normal, shutdown}) +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), - handler_terminate(Req, Handler, HandlerState, Reason), + _ = terminate(Req, Env, Handler, State, Reason), erlang:Class([ {reason, Reason}, {mfa, {Handler, handle, 2}}, {stacktrace, Stacktrace}, {req, cowboy_req:to_list(Req)}, - {state, HandlerState} + {state, State} ]) end. -%% Update the state if the response was sent in the callback. --spec handler_after_callback(Req, #state{}, module(), any()) - -> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]} - when Req::cowboy_req:req(). -handler_after_callback(Req, State=#state{resp_sent=false}, Handler, - HandlerState) -> - receive - {cowboy_req, resp_sent} -> - handler_before_loop(Req, State#state{resp_sent=true}, Handler, - HandlerState) - after 0 -> - handler_before_loop(Req, State, Handler, HandlerState) - end; -handler_after_callback(Req, State, Handler, HandlerState) -> - handler_before_loop(Req, State, Handler, HandlerState). - --spec handler_before_loop(Req, #state{}, module(), any()) - -> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]} - when Req::cowboy_req:req(). -handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) -> - [Socket, Transport] = cowboy_req:get([socket, transport], Req), - Transport:setopts(Socket, [{active, once}]), - {suspend, ?MODULE, handler_loop, - [Req, State#state{hibernate=false}, Handler, HandlerState]}; -handler_before_loop(Req, State, Handler, HandlerState) -> - [Socket, Transport] = cowboy_req:get([socket, transport], Req), - Transport:setopts(Socket, [{active, once}]), - handler_loop(Req, State, Handler, HandlerState). - -%% Almost the same code can be found in cowboy_websocket. --spec handler_loop_timeout(#state{}) -> #state{}. -handler_loop_timeout(State=#state{loop_timeout=infinity}) -> - State#state{loop_timeout_ref=undefined}; -handler_loop_timeout(State=#state{loop_timeout=Timeout, - loop_timeout_ref=PrevRef}) -> - _ = case PrevRef of - undefined -> ignore; - PrevRef -> erlang:cancel_timer(PrevRef) +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, - TRef = erlang:start_timer(Timeout, self(), ?MODULE), - State#state{loop_timeout_ref=TRef}. - --spec handler_loop(Req, #state{}, module(), any()) - -> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]} - when Req::cowboy_req:req(). -handler_loop(Req, State=#state{loop_buffer_size=NbBytes, - loop_max_buffer=Threshold, loop_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 -> - _ = handler_terminate(Req, Handler, HandlerState, - {error, overflow}), - _ = if RespSent -> ok; true -> - cowboy_req:reply(500, Req) - end, - exit(normal); - true -> - Req2 = cowboy_req:append_buffer(Data, Req), - State2 = handler_loop_timeout(State#state{ - loop_buffer_size=NbBytes2}), - handler_before_loop(Req2, State2, Handler, HandlerState) + 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 + true -> + try + Handler:terminate(Reason, cowboy_req:lock(Req), State) + catch Class:Reason2 -> + erlang:Class([ + {reason, Reason2}, + {mfa, {Handler, terminate, 3}}, + {stacktrace, erlang:get_stacktrace()}, + {req, cowboy_req:to_list(Req)}, + {state, State}, + {terminate_reason, Reason} + ]) end; - {Closed, Socket} -> - terminate_request(Req, State, Handler, HandlerState, - {error, closed}); - {Error, Socket, Reason} -> - terminate_request(Req, State, Handler, HandlerState, - {error, Reason}); - {timeout, TRef, ?MODULE} -> - handler_after_loop(Req, State, Handler, HandlerState, - {normal, timeout}); - {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> - handler_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, - handler_call(Req2, State, Handler, HandlerState, Message) - end. - --spec handler_call(Req, #state{}, module(), any(), any()) - -> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]} - when Req::cowboy_req:req(). -handler_call(Req, State=#state{resp_sent=RespSent}, - Handler, HandlerState, Message) -> - try Handler:info(Message, Req, HandlerState) of - {ok, Req2, HandlerState2} -> - handler_after_loop(Req2, State, Handler, HandlerState2, - {normal, shutdown}); - {loop, Req2, HandlerState2} -> - handler_after_callback(Req2, State, Handler, HandlerState2); - {loop, Req2, HandlerState2, hibernate} -> - handler_after_callback(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, - handler_terminate(Req, Handler, HandlerState, Reason), - 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. --spec handler_after_loop(Req, #state{}, module(), any(), - {normal, timeout | shutdown} | {error, atom()}) -> - {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). -handler_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_request(Req2, State, Handler, HandlerState, Reason). - --spec terminate_request(Req, #state{}, module(), any(), - {normal, timeout | shutdown} | {error, atom()}) -> - {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). -terminate_request(Req, #state{env=Env, loop_timeout_ref=TRef}, - Handler, HandlerState, Reason) -> - HandlerRes = handler_terminate(Req, Handler, HandlerState, Reason), - _ = case TRef of - undefined -> ignore; - TRef -> erlang:cancel_timer(TRef) + false -> + ok end, - flush_timeouts(), - {ok, Req, [{result, HandlerRes}|Env]}. - --spec handler_terminate(cowboy_req:req(), module(), any(), - {normal, timeout | shutdown} | {error, atom()}) -> ok. -handler_terminate(Req, Handler, HandlerState, Reason) -> - try - Handler:terminate(Reason, cowboy_req:lock(Req), HandlerState) - catch Class:Reason2 -> - erlang:Class([ - {reason, Reason2}, - {mfa, {Handler, terminate, 3}}, - {stacktrace, erlang:get_stacktrace()}, - {req, cowboy_req:to_list(Req)}, - {state, HandlerState}, - {terminate_reason, Reason} - ]) - end. - --spec flush_timeouts() -> ok. -flush_timeouts() -> - receive - {timeout, TRef, ?MODULE} when is_reference(TRef) -> - flush_timeouts() - after 0 -> - ok - end. + {ok, Req, [{result, Result}|Env]}. diff --git a/src/cowboy_http_handler.erl b/src/cowboy_http_handler.erl index 14c7987..75b41d2 100644 --- a/src/cowboy_http_handler.erl +++ b/src/cowboy_http_handler.erl @@ -16,22 +16,21 @@ -type opts() :: any(). -type state() :: any(). --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()}. +%% @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({atom(), http}, Req, opts()) - -> {ok, Req, state()} - | {loop, Req, state()} - | {loop, Req, state(), hibernate} - | {loop, Req, state(), timeout()} - | {loop, Req, state(), timeout(), hibernate} +-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()} - | {upgrade, protocol, module()} - | {upgrade, protocol, module(), Req, opts()} when Req::cowboy_req:req(). -callback handle(Req, State) -> {ok, Req, State} when Req::cowboy_req:req(), State::state(). --callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. +%% @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 new file mode 100644 index 0000000..6616a78 --- /dev/null +++ b/src/cowboy_long_polling.erl @@ -0,0 +1,191 @@ +%% 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_handler.erl b/src/cowboy_loop_handler.erl index edef77f..5e50c34 100644 --- a/src/cowboy_loop_handler.erl +++ b/src/cowboy_loop_handler.erl @@ -16,25 +16,24 @@ -type opts() :: any(). -type state() :: any(). --type terminate_reason() :: {normal, shutdown} - | {normal, timeout} - | {error, closed} - | {error, overflow} - | {error, atom()}. +%% @todo see terminate +%-type terminate_reason() :: {normal, shutdown} +% | {normal, timeout} +% | {error, closed} +% | {error, overflow} +% | {error, atom()}. --callback init({atom(), http}, Req, opts()) - -> {ok, Req, state()} - | {loop, Req, state()} - | {loop, Req, state(), hibernate} - | {loop, Req, state(), timeout()} - | {loop, Req, state(), timeout(), hibernate} +-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()} - | {upgrade, protocol, module()} - | {upgrade, protocol, module(), Req, opts()} 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(). --callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. +%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 23a3868..a6e2de5 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -83,9 +83,6 @@ -export([get/2]). -export([set/2]). -export([set_bindings/4]). - -%% Misc API. --export([compact/1]). -export([lock/1]). -export([to_list/1]). @@ -1002,13 +999,6 @@ set_bindings(HostInfo, PathInfo, Bindings, Req) -> Req#http_req{host_info=HostInfo, path_info=PathInfo, bindings=Bindings}. -%% Misc API. - --spec compact(Req) -> Req when Req::req(). -compact(Req) -> - Req#http_req{host_info=undefined, path_info=undefined, - bindings=undefined, headers=[]}. - -spec lock(Req) -> Req when Req::req(). lock(Req) -> Req#http_req{resp_state=locked}. diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 4ea3010..63f33e6 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -17,7 +17,7 @@ -module(cowboy_rest). -behaviour(cowboy_sub_protocol). --export([upgrade/4]). +-export([upgrade/6]). -record(state, { env :: cowboy_middleware:env(), @@ -55,31 +55,12 @@ expires :: undefined | no_call | calendar:datetime() | binary() }). --spec upgrade(Req, Env, module(), any()) +-spec upgrade(Req, Env, module(), any(), infinity, run) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). -upgrade(Req, Env, Handler, HandlerOpts) -> +upgrade(Req, Env, Handler, HandlerState, infinity, run) -> Method = cowboy_req:method(Req), - case erlang:function_exported(Handler, rest_init, 2) of - true -> - try Handler:rest_init(Req, HandlerOpts) of - {ok, Req2, HandlerState} -> - service_available(Req2, #state{env=Env, method=Method, - handler=Handler, handler_state=HandlerState}) - catch Class:Reason -> - Stacktrace = erlang:get_stacktrace(), - cowboy_req:maybe_reply(Stacktrace, Req), - erlang:Class([ - {reason, Reason}, - {mfa, {Handler, rest_init, 2}}, - {stacktrace, Stacktrace}, - {req, cowboy_req:to_list(Req)}, - {opts, HandlerOpts} - ]) - end; - false -> - service_available(Req, #state{env=Env, method=Method, - handler=Handler}) - end. + service_available(Req, #state{env=Env, method=Method, + handler=Handler, handler_state=HandlerState}). service_available(Req, State) -> expect(Req, State, service_available, true, fun known_methods/2, 503). @@ -986,13 +967,9 @@ next(Req, State, StatusCode) when is_integer(StatusCode) -> respond(Req, State, StatusCode) -> terminate(cowboy_req:reply(StatusCode, Req), State). -terminate(Req, State=#state{env=Env}) -> - rest_terminate(Req, State), - {ok, Req, [{result, ok}|Env]}. - error_terminate(Req, State=#state{handler=Handler, handler_state=HandlerState}, Class, Reason, Callback) -> - rest_terminate(Req, State), + _ = terminate(Req, State), Stacktrace = erlang:get_stacktrace(), cowboy_req:maybe_reply(Stacktrace, Req), erlang:Class([ @@ -1003,9 +980,5 @@ error_terminate(Req, State=#state{handler=Handler, handler_state=HandlerState}, {state, HandlerState} ]). -rest_terminate(Req, #state{handler=Handler, handler_state=HandlerState}) -> - case erlang:function_exported(Handler, rest_terminate, 2) of - true -> ok = Handler:rest_terminate( - cowboy_req:lock(Req), HandlerState); - false -> ok - end. +terminate(Req, #state{env=Env, handler=Handler, handler_state=HandlerState}) -> + cowboy_handler:terminate(Req, Env, Handler, HandlerState, {normal, shutdown}). diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index 1d5286e..ecca6fa 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -15,8 +15,7 @@ -module(cowboy_static). --export([init/3]). --export([rest_init/2]). +-export([init/2]). -export([malformed_request/2]). -export([forbidden/2]). -export([content_types_provided/2]). @@ -39,33 +38,27 @@ -type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}. --spec init(_, _, _) -> {upgrade, protocol, cowboy_rest}. -init(_, _, _) -> - {upgrade, protocol, cowboy_rest}. - %% Resolve the file that will be sent and get its file information. %% If the handler is configured to manage a directory, check that the %% requested file is inside the configured directory. --spec rest_init(Req, opts()) - -> {ok, Req, error | state()} - when Req::cowboy_req:req(). -rest_init(Req, {Name, Path}) -> - rest_init_opts(Req, {Name, Path, []}); -rest_init(Req, {Name, App, Path}) +-spec init(Req, opts()) -> {rest, Req, error | state()} when Req::cowboy_req:req(). +init(Req, {Name, Path}) -> + init_opts(Req, {Name, Path, []}); +init(Req, {Name, App, Path}) when Name =:= priv_file; Name =:= priv_dir -> - rest_init_opts(Req, {Name, App, Path, []}); -rest_init(Req, Opts) -> - rest_init_opts(Req, Opts). - -rest_init_opts(Req, {priv_file, App, Path, Extra}) -> - rest_init_info(Req, absname(priv_path(App, Path)), Extra); -rest_init_opts(Req, {file, Path, Extra}) -> - rest_init_info(Req, absname(Path), Extra); -rest_init_opts(Req, {priv_dir, App, Path, Extra}) -> - rest_init_dir(Req, priv_path(App, Path), Extra); -rest_init_opts(Req, {dir, Path, Extra}) -> - rest_init_dir(Req, Path, Extra). + init_opts(Req, {Name, App, Path, []}); +init(Req, Opts) -> + init_opts(Req, Opts). + +init_opts(Req, {priv_file, App, Path, Extra}) -> + init_info(Req, absname(priv_path(App, Path)), Extra); +init_opts(Req, {file, Path, Extra}) -> + init_info(Req, absname(Path), Extra); +init_opts(Req, {priv_dir, App, Path, Extra}) -> + init_dir(Req, priv_path(App, Path), Extra); +init_opts(Req, {dir, Path, Extra}) -> + init_dir(Req, Path, Extra). priv_path(App, Path) -> case code:priv_dir(App) of @@ -83,18 +76,18 @@ absname(Path) when is_list(Path) -> absname(Path) when is_binary(Path) -> filename:absname(Path). -rest_init_dir(Req, Path, Extra) when is_list(Path) -> - rest_init_dir(Req, list_to_binary(Path), Extra); -rest_init_dir(Req, Path, Extra) -> +init_dir(Req, Path, Extra) when is_list(Path) -> + init_dir(Req, list_to_binary(Path), Extra); +init_dir(Req, Path, Extra) -> Dir = fullpath(filename:absname(Path)), PathInfo = cowboy_req:path_info(Req), Filepath = filename:join([Dir|PathInfo]), Len = byte_size(Dir), case fullpath(Filepath) of << Dir:Len/binary, $/, _/binary >> -> - rest_init_info(Req, Filepath, Extra); + init_info(Req, Filepath, Extra); _ -> - {ok, Req, error} + {rest, Req, error} end. fullpath(Path) -> @@ -110,9 +103,9 @@ fullpath([<<"..">>|Tail], [_|Acc]) -> fullpath([Segment|Tail], Acc) -> fullpath(Tail, [Segment|Acc]). -rest_init_info(Req, Path, Extra) -> +init_info(Req, Path, Extra) -> Info = file:read_file_info(Path, [{time, universal}]), - {ok, Req, {Path, Info, Extra}}. + {rest, Req, {Path, Info, Extra}}. -ifdef(TEST). fullpath_test_() -> diff --git a/src/cowboy_sub_protocol.erl b/src/cowboy_sub_protocol.erl index 713c3cd..f177263 100644 --- a/src/cowboy_sub_protocol.erl +++ b/src/cowboy_sub_protocol.erl @@ -15,9 +15,6 @@ -module(cowboy_sub_protocol). --callback upgrade(Req, Env, module(), any()) - -> {ok, Req, Env} - | {suspend, module(), atom(), [any()]} - | {halt, Req} - | {error, cowboy:http_status(), Req} +-callback upgrade(Req, Env, module(), any(), timeout(), run | hibernate) + -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {halt, Req} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index 1c115b5..e0b20ca 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -17,7 +17,7 @@ -module(cowboy_websocket). -behaviour(cowboy_sub_protocol). --export([upgrade/4]). +-export([upgrade/6]). -export([handler_loop/4]). -type close_code() :: 1000..4999. @@ -53,19 +53,18 @@ deflate_state :: undefined | port() }). --spec upgrade(Req, Env, module(), any()) - -> {ok, Req, Env} - | {suspend, module(), atom(), [any()]} +-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, HandlerOpts) -> +upgrade(Req, Env, Handler, HandlerState, Timeout, Hibernate) -> {_, Ref} = lists:keyfind(listener, 1, Env), ranch:remove_connection(Ref), [Socket, Transport] = cowboy_req:get([socket, transport], Req), - State = #state{env=Env, socket=Socket, transport=Transport, - handler=Handler}, + State = #state{env=Env, socket=Socket, transport=Transport, handler=Handler, + timeout=Timeout, hibernate=Hibernate =:= hibernate}, try websocket_upgrade(State, Req) of {ok, State2, Req2} -> - handler_init(State2, Req2, HandlerOpts) + websocket_handshake(State2, Req2, HandlerState) catch _:_ -> receive {cowboy_req, resp_sent} -> ok @@ -121,38 +120,6 @@ websocket_extensions(State, Req) -> end end. --spec handler_init(#state{}, Req, any()) - -> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]} - when Req::cowboy_req:req(). -handler_init(State=#state{env=Env, transport=Transport, - handler=Handler}, Req, HandlerOpts) -> - try Handler:websocket_init(Transport:name(), Req, HandlerOpts) of - {ok, Req2, HandlerState} -> - websocket_handshake(State, Req2, HandlerState); - {ok, Req2, HandlerState, hibernate} -> - websocket_handshake(State#state{hibernate=true}, - Req2, HandlerState); - {ok, Req2, HandlerState, Timeout} -> - websocket_handshake(State#state{timeout=Timeout}, - Req2, HandlerState); - {ok, Req2, HandlerState, Timeout, hibernate} -> - websocket_handshake(State#state{timeout=Timeout, - hibernate=true}, Req2, HandlerState); - {shutdown, Req2} -> - cowboy_req:ensure_response(Req2, 400), - {ok, Req2, [{result, closed}|Env]} - catch Class:Reason -> - Stacktrace = erlang:get_stacktrace(), - cowboy_req:maybe_reply(Stacktrace, Req), - erlang:Class([ - {reason, Reason}, - {mfa, {Handler, websocket_init, 3}}, - {stacktrace, Stacktrace}, - {req, cowboy_req:to_list(Req)}, - {opts, HandlerOpts} - ]) - end. - -spec websocket_handshake(#state{}, Req, any()) -> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]} @@ -703,6 +670,15 @@ websocket_send({Type, Payload0}, State=#state{socket=Socket, transport=Transport {Transport:send(Socket, [<< 1:1, Rsv/bits, Opcode:4, 0:1, BinLen/bits >>, Payload]), State2}. +-spec payload_length_to_binary(0..16#7fffffffffffffff) + -> << _:7 >> | << _:23 >> | << _:71 >>. +payload_length_to_binary(N) -> + case N of + N when N =< 125 -> << N:7 >>; + N when N =< 16#ffff -> << 126:7, N:16 >>; + N when N =< 16#7fffffffffffffff -> << 127:7, N:64 >> + end. + -spec websocket_send_many([frame()], #state{}) -> {ok, #state{}} | {shutdown, #state{}} | {{error, atom()}, #state{}}. websocket_send_many([], State) -> @@ -739,26 +715,6 @@ websocket_close(State=#state{socket=Socket, transport=Transport}, -> {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). handler_terminate(#state{env=Env, handler=Handler}, - Req, HandlerState, TerminateReason) -> - try - Handler:websocket_terminate(TerminateReason, Req, HandlerState) - catch Class:Reason -> - erlang:Class([ - {reason, Reason}, - {mfa, {Handler, websocket_terminate, 3}}, - {stacktrace, erlang:get_stacktrace()}, - {req, cowboy_req:to_list(Req)}, - {state, HandlerState}, - {terminate_reason, TerminateReason} - ]) - end, + Req, HandlerState, Reason) -> + _ = cowboy_handler:terminate(Req, Env, Handler, HandlerState, Reason), {ok, Req, [{result, closed}|Env]}. - --spec payload_length_to_binary(0..16#7fffffffffffffff) - -> << _:7 >> | << _:23 >> | << _:71 >>. -payload_length_to_binary(N) -> - case N of - N when N =< 125 -> << N:7 >>; - N when N =< 16#ffff -> << 126:7, N:16 >>; - N when N =< 16#7fffffffffffffff -> << 127:7, N:64 >> - end. diff --git a/src/cowboy_websocket_handler.erl b/src/cowboy_websocket_handler.erl index 177e5f6..a6a036b 100644 --- a/src/cowboy_websocket_handler.erl +++ b/src/cowboy_websocket_handler.erl @@ -16,21 +16,23 @@ -type opts() :: any(). -type state() :: any(). --type terminate_reason() :: {normal, shutdown} - | {normal, timeout} - | {error, closed} - | {remote, closed} - | {remote, cowboy_websocket:close_code(), binary()} - | {error, badencoding} - | {error, badframe} - | {error, atom()}. +%% @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 websocket_init(atom(), Req, opts()) - -> {ok, Req, state()} - | {ok, Req, state(), hibernate} - | {ok, Req, state(), timeout()} - | {ok, Req, state(), timeout(), hibernate} - | {shutdown, Req} +-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} @@ -46,5 +48,4 @@ | {reply, cowboy_websocket:frame() | [cowboy_websocket:frame()], Req, State, hibernate} | {shutdown, Req, State} when Req::cowboy_req:req(), State::state(). --callback websocket_terminate(terminate_reason(), cowboy_req:req(), state()) - -> ok. +%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok. -- cgit v1.2.3