From bf19e27a0f96c1c35292a6abd7a5bc1bdeec023c Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 19 Sep 2024 19:31:41 -0700 Subject: [PATCH] Route unknown error to parent exception's handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- spec/exception_handler_spec.cr | 23 +++++++++++++++++++++++ spec/spec_helper.cr | 3 +++ src/kemal/exception_handler.cr | 24 +++++++++++++++++++++--- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/spec/exception_handler_spec.cr b/spec/exception_handler_spec.cr index 916eff5f..5aa22789 100644 --- a/spec/exception_handler_spec.cr +++ b/spec/exception_handler_spec.cr @@ -129,6 +129,29 @@ describe "Kemal::ExceptionHandler" do response.body.should eq "A custom exception of CustomExceptionType has occurred" end + it "renders custom error for a child of a custom exception" do + error CustomExceptionType do |env, error| + "A custom exception of #{error.class} has occurred" + end + + get "/" do + raise ChildCustomExceptionType.new + end + + request = HTTP::Request.new("GET", "/") + io = IO::Memory.new + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE + Kemal::ExceptionHandler::INSTANCE.call(context) + response.close + io.rewind + response = HTTP::Client::Response.from_io(io, decompress: false) + response.status_code.should eq 500 + response.headers["Content-Type"].should eq "text/html" + response.body.should eq "A custom exception of ChildCustomExceptionType has occurred" + end + it "overrides the content type for filters" do before_get do |env| env.response.content_type = "application/json" diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 1fce0b94..8ce02fae 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -29,6 +29,9 @@ end class CustomExceptionType < Exception end +class ChildCustomExceptionType < CustomExceptionType +end + add_context_storage_type(TestContextStorageType) add_context_storage_type(AnotherContextStorageType) diff --git a/src/kemal/exception_handler.cr b/src/kemal/exception_handler.cr index dbf8b88f..4b7e83a3 100644 --- a/src/kemal/exception_handler.cr +++ b/src/kemal/exception_handler.cr @@ -13,6 +13,14 @@ module Kemal rescue ex : Exception # Use error handler defined for the current exception if it exists return call_exception_with_exception(context, ex, 500) if Kemal.config.error_handlers.has_key?(ex.class) + + # Use error handler for an ancestor of the current exception if it exists + Kemal.config.error_handlers.each_key do |key| + if key.is_a? Exception.class && ex.class <= key + return call_exception_with_exception(context, ex, 500, override_handler_used: key) + end + end + log("Exception: #{ex.inspect_with_backtrace}") # Else use generic 500 handler if defined return call_exception_with_status_code(context, ex, 500) if Kemal.config.error_handlers.has_key?(500) @@ -20,13 +28,23 @@ module Kemal render_500(context, ex, verbosity) end - private def call_exception_with_exception(context : HTTP::Server::Context, exception : Exception, status_code : Int32 = 500) + # Calls the defined error handler for the given exception if it exists + # + # By default it tries to use the handler that is defined for the given exception. However, another + # handler can be used via the `override_handler_used` parameter. + private def call_exception_with_exception(context : HTTP::Server::Context, exception : Exception, status_code : Int32 = 500, override_handler_used : (Exception.class)? = nil) return if context.response.closed? - if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(exception.class) + if !override_handler_used + handler_to_use = exception.class + else + handler_to_use = override_handler_used + end + + if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(handler_to_use) context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type") context.response.status_code = status_code - context.response.print Kemal.config.error_handlers[exception.class].call(context, exception) + context.response.print Kemal.config.error_handlers[handler_to_use].call(context, exception) context end end