-module(nova_handler). -behaviour(cowboy_middleware). %% Callbacks -export([ execute/2, terminate/3 ]). -include_lib("kernel/include/logger.hrl"). -include("nova_router.hrl"). -callback init(Req, any()) -> {ok | module(), Req, any()} | {module(), Req, any(), any()} when Req::cowboy_req:req(). -callback terminate(any(), map(), any()) -> ok. -optional_callbacks([terminate/3]). -spec execute(Req, Env) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). execute(Req, Env = #{cowboy_handler := Handler, arguments := Arguments}) -> try erlang:apply(Handler, init, [Req, Arguments]) of {ok, Req2, _State} -> Result = terminate(normal, Req2, Handler), {ok, Req2, Env#{result => Result}}; {Mod, Req2, State} -> erlang:apply(Mod, upgrade, [Req2, Env, Handler, State]); {Mod, Req2, State, Opts} -> erlang:apply(Mod, upgrade, [Req2, Env, Handler, State, Opts]) catch Class:Reason:Stacktrace -> Payload = #{status_code => 500, stacktrace => Stacktrace, class => Class, reason => Reason}, logger:error(#{msg => "Controller crashed", class => Class, reason => Reason, stacktrace => Stacktrace}), render_response(Req#{crash_info => Payload}, Env, 500) end; execute(Req, Env = #{module := Module, function := Function}) -> %% Ensure that the module exists and have the correct function exported case lists:search(fun({Export, Arity}) -> Export == Function andalso Arity == 1 end, erlang:apply(Module, module_info, [exports])) of {value, _} -> try erlang:apply(Module, Function, [Req]) of RetObj -> case nova_handlers:get_handler(element(1, RetObj)) of {ok, Callback} -> {ok, Req0} = Callback(RetObj, {Module, Function}, Req), render_response(Req0, Env); {error, not_found} -> logger:error(#{msg => "Controller returned unsupported result", controller => Module, function => Function, return => RetObj}) end catch Class:Reason:Stacktrace -> logger:error(#{msg => "Controller crashed", class => Class, reason => Reason, stacktrace => Stacktrace}), terminate(Reason, Req, Module), %% Build the payload object Payload = #{status_code => 500, stacktrace => Stacktrace, class => Class, reason => Reason}, render_response(Req#{crash_info => Payload}, Env, 500) end; _ -> logger:error(#{msg => "Could not find controller", controller => Module, function => io_lib:format("~s/1", [Function])}), Payload = #{status_code => 500, status => "Problems with application", stacktrace => [{Module, Function, 1}], title => "Woops. It seems like you have a problem with your application!", extra_msg => "Nova could not find your controller:function. Pleae check your spelling and/or that the module" ++ " have been properly loaded. " }, render_response(Req#{crash_info => Payload}, Env, 500) end. -spec terminate(any(), Req | undefined, module()) -> ok when Req::cowboy_req:req(). terminate(Reason, Req, Module) -> case erlang:function_exported(Module, terminate, 3) of true -> erlang:apply(Module, terminate, [Reason, Req]); false -> ok end. %%%%%%%%%%%%%%%%%%%%%%%% %% INTERNAL FUNCTIONS %% %%%%%%%%%%%%%%%%%%%%%%%% -spec render_response(Req :: cowboy_req:req(), Env :: map()) -> {ok, Req :: cowboy_req:req(), State :: map()}. render_response(Req = #{resp_status_code := StatusCode}, Env) -> Req0 = cowboy_req:reply(StatusCode, Req), {ok, Req0, Env}; render_response(Req, Env) -> Req0 = cowboy_req:reply(200, Req), {ok, Req0, Env}. render_response(Req, Env, StatusCode) -> case application:get_env(nova, render_error_pages, true) of true -> case nova_router:lookup_url(StatusCode) of {error, _} -> %% Render the internal view of nova {ok, Req0} = nova_basic_handler:handle_status({status, StatusCode}, {dummy, dummy}, Req), render_response(Req0, Env); {ok, _Bindings, #nova_handler_value{module = EMod, function = EFunc}} -> %% Recurse to the execute function to render error page execute(Req, Env#{app => nova, module => EMod, function => EFunc}) end; false -> {ok, Req0} = nova_basic_handler:handle_status({status, StatusCode}, {dummy, dummy}, Req), render_response(Req0, Env) end.