From 1b3f510b7e8d5413901ba72adfe361773f3e9097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Thu, 3 Jan 2013 22:47:51 +0100 Subject: Add middleware support Middlewares allow customizing the request processing. All existing Cowboy project are incompatible with this commit. You need to change `{dispatch, Dispatch}` in the protocol options to `{env, [{dispatch, Dispatch}]}` to fix your code. --- src/cowboy_handler.erl | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/cowboy_handler.erl (limited to 'src/cowboy_handler.erl') diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl new file mode 100644 index 0000000..cc871d9 --- /dev/null +++ b/src/cowboy_handler.erl @@ -0,0 +1,201 @@ +%% Copyright (c) 2011-2013, 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. + +%% @doc Handler middleware. +%% +%% 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. +%% +%% @see cowboy_http_handler +-module(cowboy_handler). +-behaviour(cowboy_middleware). + +-export([execute/2]). +-export([handler_loop/4]). + +-record(state, { + env :: cowboy_middleware:env(), + hibernate = false :: boolean(), + loop_timeout = infinity :: timeout(), + loop_timeout_ref :: undefined | reference() +}). + +%% @private +-spec execute(Req, Env) + -> {ok, Req, Env} | {error, 500, Req} + | {suspend, ?MODULE, handler_loop, [any()]} + 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), + handler_init(Req, #state{env=Env}, Handler, HandlerOpts). + +-spec handler_init(Req, #state{}, module(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [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_before_loop(Req2, State#state{hibernate=false}, + Handler, HandlerState); + {loop, Req2, HandlerState, hibernate} -> + handler_before_loop(Req2, State#state{hibernate=true}, + Handler, HandlerState); + {loop, Req2, HandlerState, Timeout} -> + handler_before_loop(Req2, State#state{loop_timeout=Timeout}, + Handler, HandlerState); + {loop, Req2, HandlerState, Timeout, hibernate} -> + handler_before_loop(Req2, State#state{ + hibernate=true, loop_timeout=Timeout}, Handler, HandlerState); + {shutdown, Req2, HandlerState} -> + terminate_request(Req2, State, Handler, HandlerState); + %% @todo {upgrade, transport, Module} + {upgrade, protocol, Module} -> + upgrade_protocol(Req, State, Handler, HandlerOpts, Module); + {upgrade, protocol, Module, Req2, HandlerOpts2} -> + upgrade_protocol(Req2, State, Handler, HandlerOpts2, Module) + catch Class:Reason -> + error_logger:error_msg( + "** Cowboy handler ~p terminating in ~p/~p~n" + " for the reason ~p:~p~n" + "** Options were ~p~n" + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, init, 3, Class, Reason, HandlerOpts, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + {error, 500, Req} + end. + +-spec upgrade_protocol(Req, #state{}, module(), any(), module()) + -> {ok, Req, Env} + | {suspend, module(), atom(), any()} + | {halt, Req} + | {error, cowboy_http:status(), 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()} + | {error, 500, Req} + 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) + catch Class:Reason -> + error_logger:error_msg( + "** Cowboy handler ~p terminating in ~p/~p~n" + " for the reason ~p:~p~n" + "** Handler state was ~p~n" + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, handle, 2, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + handler_terminate(Req, Handler, HandlerState), + {error, 500, Req} + end. + +%% We don't listen for Transport closes because that would force us +%% to receive data and buffer it indefinitely. +-spec handler_before_loop(Req, #state{}, module(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [any()]} + when Req::cowboy_req:req(). +handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) -> + State2 = handler_loop_timeout(State), + {suspend, ?MODULE, handler_loop, + [Req, State2#state{hibernate=false}, Handler, HandlerState]}; +handler_before_loop(Req, State, Handler, HandlerState) -> + State2 = handler_loop_timeout(State), + handler_loop(Req, State2, 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) end, + TRef = erlang:start_timer(Timeout, self(), ?MODULE), + State#state{loop_timeout_ref=TRef}. + +%% @private +-spec handler_loop(Req, #state{}, module(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [any()]} + when Req::cowboy_req:req(). +handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) -> + receive + {timeout, TRef, ?MODULE} -> + terminate_request(Req, State, Handler, HandlerState); + {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> + handler_loop(Req, State, Handler, HandlerState); + Message -> + handler_call(Req, State, Handler, HandlerState, Message) + end. + +-spec handler_call(Req, #state{}, module(), any(), any()) + -> {ok, Req, cowboy_middleware:env()} + | {error, 500, Req} | {suspend, module(), function(), [any()]} + when Req::cowboy_req:req(). +handler_call(Req, State, Handler, HandlerState, Message) -> + try Handler:info(Message, Req, HandlerState) of + {ok, Req2, HandlerState2} -> + terminate_request(Req2, State, Handler, HandlerState2); + {loop, Req2, HandlerState2} -> + handler_before_loop(Req2, State, Handler, HandlerState2); + {loop, Req2, HandlerState2, hibernate} -> + handler_before_loop(Req2, State#state{hibernate=true}, + Handler, HandlerState2) + catch Class:Reason -> + error_logger:error_msg( + "** Cowboy handler ~p terminating in ~p/~p~n" + " for the reason ~p:~p~n" + "** Handler state was ~p~n" + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, info, 3, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + handler_terminate(Req, Handler, HandlerState), + {error, 500, Req} + end. + +-spec terminate_request(Req, #state{}, module(), any()) -> + {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). +terminate_request(Req, #state{env=Env}, Handler, HandlerState) -> + HandlerRes = handler_terminate(Req, Handler, HandlerState), + {ok, Req, [{result, HandlerRes}|Env]}. + +-spec handler_terminate(cowboy_req:req(), module(), any()) -> ok. +handler_terminate(Req, Handler, HandlerState) -> + try + Handler:terminate(cowboy_req:lock(Req), HandlerState) + catch Class:Reason -> + error_logger:error_msg( + "** Cowboy handler ~p terminating in ~p/~p~n" + " for the reason ~p:~p~n" + "** Handler state was ~p~n" + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, terminate, 2, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]) + end. -- cgit v1.2.3