|
@@ -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]}.
|