diff --git a/lib/utopia/exceptions/application_errors.rb b/lib/utopia/exceptions/application_errors.rb new file mode 100644 index 00000000..0b8f038f --- /dev/null +++ b/lib/utopia/exceptions/application_errors.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +module Utopia + module Exceptions + # Exceptions raised by application code which can be safely handled and reported. + APPLICATION_ERRORS = [StandardError, ScriptError].freeze + end +end diff --git a/lib/utopia/exceptions/handler.rb b/lib/utopia/exceptions/handler.rb index a6bca4d1..c6cdae88 100644 --- a/lib/utopia/exceptions/handler.rb +++ b/lib/utopia/exceptions/handler.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2014-2025, by Samuel Williams. +# Copyright, 2014-2026, by Samuel Williams. # Copyright, 2025, by Olle Jonsson. require "console" @@ -9,6 +9,7 @@ require_relative "../middleware" require_relative "../request" require_relative "../response" +require_relative "application_errors" module Utopia module Exceptions @@ -37,7 +38,7 @@ def freeze def call(request) begin return @delegate.call(request) - rescue Exception => exception + rescue *APPLICATION_ERRORS => exception Console.warn(self, "An error occurred while processing the request.", error: exception) begin @@ -52,7 +53,7 @@ def call(request) error_response.status = 500 return error_response - rescue Exception => exception + rescue *APPLICATION_ERRORS => exception # If redirection fails, we also finish with a fatal error: Console.error(self, "An error occurred while invoking the error handler.", error: exception) return Response[500, {"content-type" => "text/plain"}, ["An error occurred while processing the request."]] diff --git a/lib/utopia/exceptions/mailer.rb b/lib/utopia/exceptions/mailer.rb index e20ae287..9da54be3 100644 --- a/lib/utopia/exceptions/mailer.rb +++ b/lib/utopia/exceptions/mailer.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2016-2025, by Samuel Williams. +# Copyright, 2016-2026, by Samuel Williams. require "net/smtp" require "mail" require_relative "../middleware" require_relative "../request" -require_relative "handler" +require_relative "application_errors" module Utopia module Exceptions @@ -59,7 +59,7 @@ def freeze def call(request) begin return @delegate.call(request) - rescue => exception + rescue *APPLICATION_ERRORS => exception request.exception = exception send_notification exception, request diff --git a/test/utopia/exceptions/.handler/controller.rb b/test/utopia/exceptions/.handler/controller.rb index 07977a94..ecc199fd 100644 --- a/test/utopia/exceptions/.handler/controller.rb +++ b/test/utopia/exceptions/.handler/controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2015-2023, by Samuel Williams. +# Copyright, 2015-2026, by Samuel Williams. prepend Actions @@ -12,6 +12,14 @@ class TharSheBlows < StandardError raise TharSheBlows.new("Arrrh!") end +on "syntax-error" do + raise SyntaxError.new("Invalid application syntax!") +end + +on "interrupt" do + raise Interrupt.new("Application interrupted!") +end + # The ExceptionHandler middleware will redirect here when an exception occurs. If this also fails, things get ugly. on "exception" do |request| if request.query_parameters["fatal"] diff --git a/test/utopia/exceptions/handler.rb b/test/utopia/exceptions/handler.rb index c4e315d3..77d008bb 100644 --- a/test/utopia/exceptions/handler.rb +++ b/test/utopia/exceptions/handler.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2015-2025, by Samuel Williams. +# Copyright, 2015-2026, by Samuel Williams. require "sus/fixtures/protocol/http/middleware_context" require "utopia/application" @@ -35,4 +35,15 @@ expect(last_response.status).to be == 500 expect(last_response.read).to be(:include?, "Error: Arrrh!") end + + it "handles application syntax errors" do + client.get "/syntax-error" + + expect(last_response.status).to be == 500 + expect(last_response.read).to be(:include?, "Invalid application syntax!") + end + + it "does not handle process exceptions" do + expect{client.get "/interrupt"}.to raise_exception(Interrupt, message: be =~ /Application interrupted/) + end end diff --git a/test/utopia/exceptions/mailer.rb b/test/utopia/exceptions/mailer.rb index ea304c58..7c07705c 100644 --- a/test/utopia/exceptions/mailer.rb +++ b/test/utopia/exceptions/mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2016-2025, by Samuel Williams. +# Copyright, 2016-2026, by Samuel Williams. require "sus/fixtures/protocol/http/middleware_context" require "utopia/application" @@ -43,6 +43,19 @@ def before expect(last_mail.to_s).to be(:include?, "TharSheBlows") end + it "reports application syntax errors" do + expect{client.get "/syntax-error"}.to raise_exception(SyntaxError, message: be =~ /Invalid application syntax/) + + last_mail = Mail::TestMailer.deliveries.last + expect(last_mail.to_s).to be(:include?, "SyntaxError") + end + + it "does not report process exceptions" do + expect{client.get "/interrupt"}.to raise_exception(Interrupt, message: be =~ /Application interrupted/) + + expect(Mail::TestMailer.deliveries).to be(:empty?) + end + it "extracts rewindable request bodies" do request = Utopia::Request["POST", "/", {}, ["Hello", " World!"]] request.body.read