diff options
Diffstat (limited to 'src/cowboy_handler.erl')
-rw-r--r-- | src/cowboy_handler.erl | 299 |
1 files changed, 49 insertions, 250 deletions
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 <em>handler</em> and <em>handler_opts</em> %% environment values. The result of this execution is added to the %% environment under the <em>result</em> 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 -%% <em>init/3</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_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]}. |