lib/roda/plugins/error_handler.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The error_handler plugin adds an error handler to the routing,
    # so that if routing the request raises an error, a nice error
    # message page can be returned to the user.
    # 
    # You can provide the error handler as a block to the plugin:
    #
    #   plugin :error_handler do |e|
    #     "Oh No!"
    #   end
    #
    # Or later via the +error+ class method:
    #
    #   plugin :error_handler
    #
    #   error do |e|
    #     "Oh No!"
    #   end
    #
    # In both cases, the exception instance is passed into the block,
    # and the block can return the request body via a string.
    #
    # If an exception is raised, a new response will be used, with the
    # default status set to 500, before executing the error handler.
    # The error handler can change the response status if necessary,
    # as well set headers and/or write to the body, just like a regular
    # request.  After the error handler returns a response, normal after
    # processing of that response occurs, except that an exception during
    # after processing is logged to <tt>env['rack.errors']</tt> but
    # otherwise ignored. This avoids recursive calls into the
    # error_handler.  Note that if the error_handler itself raises
    # an exception, the exception will be raised without normal after
    # processing.  This can cause some after processing to run twice
    # (once before the error_handler is called and once after) if
    # later after processing raises an exception.
    #
    # By default, this plugin handles StandardError and ScriptError.
    # To override the exception classes it will handle, pass a :classes
    # option to the plugin:
    #
    #   plugin :error_handler, classes: [StandardError, ScriptError, NoMemoryError]
    module ErrorHandler
      DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze

      # If a block is given, automatically call the +error+ method on
      # the Roda class with it.
      def self.configure(app, opts={}, &block)
        app.opts[:error_handler_classes] = (opts[:classes] || app.opts[:error_handler_classes] || DEFAULT_ERROR_HANDLER_CLASSES).dup.freeze

        if block
          app.error(&block)
        end
      end

      module ClassMethods
        # Install the given block as the error handler, so that if routing
        # the request raises an exception, the block will be called with
        # the exception in the scope of the Roda instance.
        def error(&block)
          define_method(:handle_error, &block)
          alias_method(:handle_error, :handle_error)
          private :handle_error
        end
      end

      module InstanceMethods
        # If an error occurs, set the response status to 500 and call
        # the error handler. Old Dispatch API.
        def call
          # RODA4: Remove
          begin
            res = super
          ensure
            _roda_after(res)
          end
        rescue *opts[:error_handler_classes] => e
          _handle_error(e)
        end

        # If an error occurs, set the response status to 500 and call
        # the error handler. 
        def _roda_handle_main_route
          begin
            res = super
          ensure
            _roda_after(res)
          end
        rescue *opts[:error_handler_classes] => e
          _handle_error(e)
        end

        private

        # Default empty implementation of _roda_after, usually
        # overridden by Roda.def_roda_before.
        def _roda_after(res)
        end

        # Handle the given exception using handle_error, using a default status
        # of 500.  Run after hooks on the rack response, but if any error occurs
        # when doing so, log the error using rack.errors and return the response.
        def _handle_error(e)
          res = @_response
          res.send(:initialize)
          res.status = 500
          res = _roda_handle_route{handle_error(e)}
          begin
            _roda_after(res)
          rescue => e2
            if errors = env['rack.errors']
              errors.puts "Error in after hook processing of error handler: #{e2.class}: #{e2.message}"
              e2.backtrace.each{|line| errors.puts(line)}
            end
          end
          res
        end

        # By default, have the error handler reraise the error, so using
        # the plugin without installing an error handler doesn't change
        # behavior.
        def handle_error(e)
          raise e
        end
      end
    end

    register_plugin(:error_handler, ErrorHandler)
  end
end