File: error.rb

package info (click to toggle)
ruby-grape 1.6.2-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 2,156 kB
  • sloc: ruby: 25,265; makefile: 7
file content (139 lines) | stat: -rw-r--r-- 5,263 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# frozen_string_literal: true

require 'grape/middleware/base'
require 'active_support/core_ext/string/output_safety'

module Grape
  module Middleware
    class Error < Base
      def default_options
        {
          default_status: 500, # default status returned on error
          default_message: '',
          format: :txt,
          helpers: nil,
          formatters: {},
          error_formatters: {},
          rescue_all: false, # true to rescue all exceptions
          rescue_grape_exceptions: false,
          rescue_subclasses: true, # rescue subclasses of exceptions listed
          rescue_options: {
            backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
            original_exception: false # true to display exception
          },
          rescue_handlers: {}, # rescue handler blocks
          base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
          all_rescue_handler: nil # rescue handler block to rescue from all exceptions
        }
      end

      def initialize(app, *options)
        super
        self.class.send(:include, @options[:helpers]) if @options[:helpers]
      end

      def call!(env)
        @env = env
        begin
          error_response(catch(:error) do
            return @app.call(@env)
          end)
        rescue Exception => e # rubocop:disable Lint/RescueException
          handler =
            rescue_handler_for_base_only_class(e.class) ||
            rescue_handler_for_class_or_its_ancestor(e.class) ||
            rescue_handler_for_grape_exception(e.class) ||
            rescue_handler_for_any_class(e.class) ||
            raise

          run_rescue_handler(handler, e)
        end
      end

      def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
        headers = headers.reverse_merge(Grape::Http::Headers::CONTENT_TYPE => content_type)
        rack_response(format_message(message, backtrace, original_exception), status, headers)
      end

      def default_rescue_handler(e)
        error_response(message: e.message, backtrace: e.backtrace, original_exception: e)
      end

      # TODO: This method is deprecated. Refactor out.
      def error_response(error = {})
        status = error[:status] || options[:default_status]
        message = error[:message] || options[:default_message]
        headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }
        headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
        backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
        original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
        rack_response(format_message(message, backtrace, original_exception), status, headers)
      end

      def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type })
        message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML
        Rack::Response.new([message], status, headers)
      end

      def format_message(message, backtrace, original_exception = nil)
        format = env[Grape::Env::API_FORMAT] || options[:format]
        formatter = Grape::ErrorFormatter.formatter_for(format, **options)
        throw :error,
              status: 406,
              message: "The requested format '#{format}' is not supported.",
              backtrace: backtrace,
              original_exception: original_exception unless formatter
        formatter.call(message, backtrace, options, env, original_exception)
      end

      private

      def rescue_handler_for_base_only_class(klass)
        error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err }

        return unless error

        handler || :default_rescue_handler
      end

      def rescue_handler_for_class_or_its_ancestor(klass)
        error, handler = options[:rescue_handlers].find { |err, _handler| klass <= err }

        return unless error

        handler || :default_rescue_handler
      end

      def rescue_handler_for_grape_exception(klass)
        return unless klass <= Grape::Exceptions::Base
        return :error_response if klass == Grape::Exceptions::InvalidVersionHeader
        return unless options[:rescue_grape_exceptions] || !options[:rescue_all]

        :error_response
      end

      def rescue_handler_for_any_class(klass)
        return unless klass <= StandardError
        return unless options[:rescue_all] || options[:rescue_grape_exceptions]

        options[:all_rescue_handler] || :default_rescue_handler
      end

      def run_rescue_handler(handler, error)
        if handler.instance_of?(Symbol)
          raise NoMethodError, "undefined method `#{handler}'" unless respond_to?(handler)

          handler = public_method(handler)
        end

        response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)

        if response.is_a?(Rack::Response)
          response
        else
          run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new)
        end
      end
    end
  end
end