diff --git a/spec/common_exception_handler_spec.cr b/spec/common_exception_handler_spec.cr index 3cb480f..f4a3877 100644 --- a/spec/common_exception_handler_spec.cr +++ b/spec/common_exception_handler_spec.cr @@ -2,10 +2,27 @@ require "./spec_helper" describe "Kemal::CommonExceptionHandler" do it "renders 404 on route not found" do - common_exception_handler = Kemal::CommonExceptionHandler::INSTANCE - request = HTTP::Request.new("GET", "/?message=world") - io_with_context = create_request_and_return_io(common_exception_handler, request) - client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) + get "/" do |env| + "Hello" + end + + request = HTTP::Request.new("GET", "/asd") + client_response = call_request_on_app(request) client_response.status_code.should eq 404 end + + it "renders custom error" do + error 403 do + "403 error" + end + + get "/" do |env| + env.response.status_code = 403 + end + + request = HTTP::Request.new("GET", "/") + client_response = call_request_on_app(request) + client_response.status_code.should eq 403 + client_response.body.should eq "403 error" + end end diff --git a/src/kemal.cr b/src/kemal.cr index 088e897..62beb49 100644 --- a/src/kemal.cr +++ b/src/kemal.cr @@ -2,7 +2,6 @@ require "./kemal/*" require "./kemal/middleware/*" module Kemal - # The command to run a `Kemal` application. def self.run Kemal::CLI.new @@ -13,6 +12,10 @@ module Kemal config.server = HTTP::Server.new(config.host_binding.not_nil!, config.port, config.handlers) config.server.not_nil!.ssl = config.ssl + error 404 do |env| + render_404(env) + end + # Test environment doesn't need to have signal trap, built-in images, and logging. unless config.env == "test" Signal::INT.trap { diff --git a/src/kemal/common_exception_handler.cr b/src/kemal/common_exception_handler.cr index 293bbeb..8ed70cb 100644 --- a/src/kemal/common_exception_handler.cr +++ b/src/kemal/common_exception_handler.cr @@ -1,18 +1,22 @@ -class Kemal::CommonExceptionHandler < HTTP::Handler - INSTANCE = new +module Kemal + class CommonExceptionHandler < HTTP::Handler + INSTANCE = new - def call(context) - begin - call_next context - rescue ex : Kemal::Exceptions::RouteNotFound - context.response.content_type = "text/html" - Kemal.config.logger.write("Exception: #{ex.inspect_with_backtrace}\n") - return render_404(context) - rescue ex - context.response.content_type = "text/html" - Kemal.config.logger.write("Exception: #{ex.inspect_with_backtrace}\n") - verbosity = Kemal.config.env == "production" ? false : true - return render_500(context, ex.inspect_with_backtrace, verbosity) + def call(context) + begin + call_next(context) + rescue ex : Kemal::Exceptions::RouteNotFound + return Kemal.config.error_handlers[404].call(context) + rescue ex1 : Kemal::Exceptions::CustomException + status_code = ex1.context.response.status_code + return Kemal.config.error_handlers[status_code].call(context) if Kemal.config.error_handlers.key?(status_code) + rescue ex2 + Kemal.config.error_handlers[500].call(context) if Kemal.config.error_handlers.key?(500) + context.response.content_type = "text/html" + Kemal.config.logger.write("Exception: #{ex2.inspect_with_backtrace}\n") + verbosity = Kemal.config.env == "production" ? false : true + return render_500(context, ex2.inspect_with_backtrace, verbosity) + end end end end diff --git a/src/kemal/config.cr b/src/kemal/config.cr index 7a092f1..1531019 100644 --- a/src/kemal/config.cr +++ b/src/kemal/config.cr @@ -1,12 +1,13 @@ module Kemal class Config - INSTANCE = Config.new - HANDLERS = [] of HTTP::Handler + INSTANCE = Config.new + HANDLERS = [] of HTTP::Handler + ERROR_HANDLERS = {} of Int32 => HTTP::Server::Context -> String @ssl : OpenSSL::SSL::Context? @server : HTTP::Server? property host_binding, ssl, port, env, public_folder, logging, - always_rescue, serve_static, server + always_rescue, serve_static, server, error_handler def initialize @host_binding = "0.0.0.0" @@ -19,8 +20,6 @@ module Kemal @error_handler = nil @always_rescue = true @run = false - @ssl = nil - @server = nil end def logger @@ -47,6 +46,14 @@ module Kemal HANDLERS << handler end + def error_handlers + ERROR_HANDLERS + end + + def add_error_handler(status_code, &handler : HTTP::Server::Context -> _) + ERROR_HANDLERS[status_code] = ->(context : HTTP::Server::Context) { handler.call(context).to_s } + end + def setup setup_log_handler setup_error_handler diff --git a/src/kemal/dsl.cr b/src/kemal/dsl.cr index 6b5d589..de16a86 100644 --- a/src/kemal/dsl.cr +++ b/src/kemal/dsl.cr @@ -9,3 +9,7 @@ HTTP_METHODS = %w(get post put patch delete options) def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) Kemal::WebSocketHandler.new path, &block end + +def error(status_code, &block : HTTP::Server::Context -> _) + Kemal.config.add_error_handler status_code, &block +end diff --git a/src/kemal/exceptions.cr b/src/kemal/exceptions.cr index b7d7275..ef5d9a6 100644 --- a/src/kemal/exceptions.cr +++ b/src/kemal/exceptions.cr @@ -4,4 +4,12 @@ module Kemal::Exceptions super "Requested path: '#{context.request.override_method as String}:#{context.request.path}' was not found." end end + + class CustomException < Exception + getter context + + def initialize(@context) + super "Rendered error with #{@context.response.status_code}" + end + end end diff --git a/src/kemal/route_handler.cr b/src/kemal/route_handler.cr index a3278d5..995ea6f 100644 --- a/src/kemal/route_handler.cr +++ b/src/kemal/route_handler.cr @@ -31,9 +31,12 @@ class Kemal::RouteHandler < HTTP::Handler # Processes the route if it's a match. Otherwise renders 404. def process_request(context) - raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_defined? + return raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_defined? route = context.route_lookup.payload as Route context.response.print(route.handler.call(context)) + if Kemal.config.error_handlers.has_key?(context.response.status_code) + return raise Kemal::Exceptions::CustomException.new(context) + end context end