rescue_registry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE +20 -0
- data/README.md +75 -0
- data/Rakefile +20 -0
- data/lib/rescue_registry.rb +51 -0
- data/lib/rescue_registry/action_dispatch.rb +71 -0
- data/lib/rescue_registry/controller.rb +48 -0
- data/lib/rescue_registry/exception_handler.rb +148 -0
- data/lib/rescue_registry/exceptions_app.rb +36 -0
- data/lib/rescue_registry/railtie.rb +8 -0
- data/lib/rescue_registry/registry.rb +100 -0
- data/lib/rescue_registry/version.rb +5 -0
- data/lib/tasks/rescue_registry_tasks.rake +4 -0
- metadata +163 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 528eb94a1296262f6cb1d20d7b8209072147c72e392dabb468fdbb0d2e4d9f43
|
4
|
+
data.tar.gz: 296ea4a7f1a08fa3fbd981c1ec08e35e528befc17b4167a51b5cdcd480783229
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 076d737ba7c477015a3b984e34d2c3782b56688d039db35b27d9cf8d89b682755e1a01c0cae34996ab63ed1fdf2a6e20c8b945d8f1b84c1a5a196f002b060b6c
|
7
|
+
data.tar.gz: 46c7d35672ecb8d4f04b5d2cad6f4d9844ef3d6d5600c6ba2eaa090a638c2bce77f180163523c5bda7ca646db6355f172ce70da996bc9fbc5b9f6d9b4bcf7191
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
No releases means no changes...
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Peter Wagenet
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# RescueRegistry
|
2
|
+
|
3
|
+
RescueRegistry improves error handling with Rails while still hewing as close to the defaults as possible.
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
### Better than `rescue_from`
|
10
|
+
`rescue_from` works fine, but it it's a big hammer. Using it completely bypasses Rail's built-in exception handling middlewares,
|
11
|
+
which actually do some nice things for us (e.g. automatically handling different data formats). RescueRegistry allows the built-in
|
12
|
+
middleware to handle the exceptions but with our custom handlers.
|
13
|
+
|
14
|
+
### Better default exception handling
|
15
|
+
Rails also has some built-in support for assigning different exception classes to status types (See `config.action_dispatch.rescue_responses`).
|
16
|
+
Unfortunately, all this allows you to do is assign a status code. If you want more complex error handling, or to use different codes in
|
17
|
+
different controllers, you're out of luck. With RescueRegistry you can register and exception with a custom handler or with different status
|
18
|
+
codes in different controllers.
|
19
|
+
|
20
|
+
## In Action
|
21
|
+
|
22
|
+
### graphiti-rails
|
23
|
+
The [graphiti-rails gem](https://github.com/wagenet/graphiti-rails) uses RescueRegistry to facilitate better JSON:API error handling, since
|
24
|
+
Rails' out-of-the-box behavior doesn't adhere to the JSON:API spec.
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Example usages:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class MyController < ActionController::Base
|
32
|
+
class CustomStatusError < StandardError; end
|
33
|
+
class CustomTitleError < StandardError; end
|
34
|
+
class DetailExceptionError < StandardError; end
|
35
|
+
class DetailProcError < StandardError; end
|
36
|
+
class DetailStringError < StandardError; end
|
37
|
+
class MetaProcError < StandardError; end
|
38
|
+
class LogFalseError < StandardError; end
|
39
|
+
class CustomHandlerError < StandardError; end
|
40
|
+
class RailsError < StandardError; end
|
41
|
+
class SubclassedError < CustomStatusError; end
|
42
|
+
|
43
|
+
class CustomErrorHandler < RescueRegistry::ExceptionHandler
|
44
|
+
def self.default_status
|
45
|
+
302
|
46
|
+
end
|
47
|
+
|
48
|
+
def title
|
49
|
+
"Custom Title"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
register_exception CustomStatusError, status: 401
|
54
|
+
register_exception CustomTitleError, title: "My Title"
|
55
|
+
register_exception DetailExceptionError, detail: :exception
|
56
|
+
register_exception DetailProcError, detail: -> (e) { e.class.name.upcase }
|
57
|
+
register_exception DetailStringError, detail: "Custom Detail"
|
58
|
+
register_exception MetaProcError, meta: -> (e) { { class_name: e.class.name.upcase } }
|
59
|
+
register_exception CustomHandlerError, handler: CustomErrorHandler
|
60
|
+
register_exception RailsError, status: 403, handler: RescueRegistry::RailsExceptionHandler
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
## Installation
|
65
|
+
Add this line to your application's Gemfile:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
gem 'rescue_registry'
|
69
|
+
```
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
We'd love to have your help improving rescue_registry, send a PR!
|
73
|
+
|
74
|
+
## License
|
75
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'yard'
|
8
|
+
require 'rspec/core/rake_task'
|
9
|
+
require 'bundler/gem_tasks'
|
10
|
+
|
11
|
+
YARD::Rake::YardocTask.new
|
12
|
+
|
13
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
14
|
+
t.rspec_opts = "--order random"
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
18
|
+
load "rails/tasks/engine.rake"
|
19
|
+
|
20
|
+
task default: :spec
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
module RescueRegistry
|
6
|
+
autoload :Controller, "rescue_registry/controller"
|
7
|
+
autoload :ExceptionsApp, "rescue_registry/exceptions_app"
|
8
|
+
autoload :ExceptionHandler, "rescue_registry/exception_handler"
|
9
|
+
autoload :RailsExceptionHandler, "rescue_registry/exception_handler"
|
10
|
+
autoload :Registry, "rescue_registry/registry"
|
11
|
+
|
12
|
+
class HandlerNotFound < StandardError; end
|
13
|
+
|
14
|
+
def self.context
|
15
|
+
Thread.current[:rescue_registry_context]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.context=(value)
|
19
|
+
Thread.current[:rescue_registry_context] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.with_context(value)
|
23
|
+
original = context
|
24
|
+
self.context = value
|
25
|
+
yield
|
26
|
+
ensure
|
27
|
+
self.context = original
|
28
|
+
end
|
29
|
+
|
30
|
+
REGISTRY_METHODS = %i[
|
31
|
+
handler_for_exception
|
32
|
+
handles_exception?
|
33
|
+
status_code_for_exception
|
34
|
+
response_for_debugging
|
35
|
+
response_for_public
|
36
|
+
]
|
37
|
+
|
38
|
+
REGISTRY_METHODS.each do |method|
|
39
|
+
define_singleton_method(method) do |*args|
|
40
|
+
return unless context.respond_to?(:rescue_registry)
|
41
|
+
context.rescue_registry.public_send(method, *args)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
ActiveSupport.on_load(:action_controller) do
|
47
|
+
include RescueRegistry::Controller
|
48
|
+
end
|
49
|
+
|
50
|
+
require 'rescue_registry/action_dispatch'
|
51
|
+
require 'rescue_registry/railtie'
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'action_dispatch'
|
2
|
+
|
3
|
+
ActionDispatch::ExceptionWrapper.class_eval do
|
4
|
+
class << self
|
5
|
+
alias_method :status_code_for_exception_without_rescue_registry, :status_code_for_exception
|
6
|
+
def status_code_for_exception(class_name)
|
7
|
+
RescueRegistry.status_code_for_exception(class_name) ||
|
8
|
+
status_code_for_exception_without_rescue_registry(class_name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Since this is for debugging only it's less critical to make changes here. The main area that matters is
|
14
|
+
# returning the correct status code. Since all this code relies upon the ExceptionWrapper which we've monkeypatched,
|
15
|
+
# it should work correctly without changes. However, we can provide more details in some areas so we hook in for that.
|
16
|
+
ActionDispatch::DebugExceptions.class_eval do
|
17
|
+
private
|
18
|
+
|
19
|
+
# `#log_error`
|
20
|
+
# TODO: We may be able to add more information, though the details remain to be determined
|
21
|
+
|
22
|
+
# `#render_for_browser_request`
|
23
|
+
# TODO: We may be able to add more information, though the details remain to be determined
|
24
|
+
|
25
|
+
# This would work without changes, but the formatting would be incorrect. Since it's for debugging only,
|
26
|
+
# we could choose to ignore it, but the detailed information would definitely be useful.
|
27
|
+
alias_method :render_for_api_request_without_rescue_registry, :render_for_api_request
|
28
|
+
def render_for_api_request(content_type, wrapper)
|
29
|
+
response = nil
|
30
|
+
|
31
|
+
if RescueRegistry.handles_exception?(wrapper.exception)
|
32
|
+
# Ideally `render_for_api_request` would be split up so we could avoid some duplication in RescueRegistry
|
33
|
+
begin
|
34
|
+
response = RescueRegistry.response_for_debugging(content_type, wrapper.exception, traces: wrapper.traces)
|
35
|
+
rescue Exception => e
|
36
|
+
# Replace the original exception (still available via `cause`) and let it get handled with default handlers
|
37
|
+
wrapper = ActionDispatch::ExceptionWrapper.new(wrapper.backtrace_cleaner, e)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if response
|
42
|
+
render(*response)
|
43
|
+
else
|
44
|
+
# One of the following is true:
|
45
|
+
# - No handler for the exception
|
46
|
+
# - No response for content_type
|
47
|
+
# - An exception while generating the response
|
48
|
+
# In any case, we go with the default here.
|
49
|
+
render_for_api_request_without_rescue_registry(content_type, wrapper)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
ActionDispatch::ShowExceptions.class_eval do
|
55
|
+
# @private
|
56
|
+
alias_method :initialize_without_rescue_registry, :initialize
|
57
|
+
|
58
|
+
# @private
|
59
|
+
def initialize(*args)
|
60
|
+
initialize_without_rescue_registry(*args)
|
61
|
+
@exceptions_app = RescueRegistry::ExceptionsApp.new(@exceptions_app)
|
62
|
+
end
|
63
|
+
|
64
|
+
alias_method :call_without_rescue_registry, :call
|
65
|
+
def call(*args)
|
66
|
+
warn "Didn't expect RescueRegistry context to be set in middleware" if RescueRegistry.context
|
67
|
+
call_without_rescue_registry(*args)
|
68
|
+
ensure
|
69
|
+
RescueRegistry.context = nil
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module RescueRegistry
|
2
|
+
module Controller
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
cattr_accessor :rescue_registry
|
7
|
+
self.rescue_registry = Registry.new(self)
|
8
|
+
|
9
|
+
class << self
|
10
|
+
delegate :register_exception, to: :rescue_registry
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def rescue_registry
|
15
|
+
self.class.rescue_registry
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_action(*args)
|
19
|
+
if RescueRegistry.context
|
20
|
+
# Controller logger isn't yet available
|
21
|
+
Rails.logger.warn "Didn't expect RescueRegistry context to be set in controller"
|
22
|
+
Rails.logger.debug caller.join("\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Setting something globally is not very nice, but it allows us to access it without
|
26
|
+
# having to change a whole lot of internal Rails APIs. This especially matters when
|
27
|
+
# getting the status code via ExceptionWrapper.
|
28
|
+
# We don't unset here so that it is available to middleware
|
29
|
+
# Unsetting happens in ActionDispatch::ShowExceptions. This _should_ be ok since we shouldn't
|
30
|
+
# be processing multiple requests in the same thread.
|
31
|
+
RescueRegistry.context = self
|
32
|
+
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
class_methods do
|
37
|
+
def inherited(subklass)
|
38
|
+
super
|
39
|
+
subklass.rescue_registry = rescue_registry.dup
|
40
|
+
subklass.rescue_registry.owner = subklass
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_exception_handler
|
44
|
+
ExceptionHandler
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module RescueRegistry
|
2
|
+
class ExceptionHandler
|
3
|
+
def self.default_status
|
4
|
+
500
|
5
|
+
end
|
6
|
+
|
7
|
+
# Use a glob for options so that unknown values won't throw errors. Also, this list could get long...
|
8
|
+
def initialize(exception, **options)
|
9
|
+
@exception = exception
|
10
|
+
|
11
|
+
status = options[:status]
|
12
|
+
if status == :passthrough
|
13
|
+
status = ActionDispatch::ExceptionWrapper.status_code_for_exception_without_rescue_registry(exception.class.name)
|
14
|
+
end
|
15
|
+
@status = status
|
16
|
+
|
17
|
+
@title = options[:title]
|
18
|
+
|
19
|
+
@detail = options[:detail]
|
20
|
+
if options[:message] && @detail.nil?
|
21
|
+
# Deprecated, from GraphitiErrors
|
22
|
+
@detail = (options[:message] == true) ? :exception : options[:message]
|
23
|
+
end
|
24
|
+
|
25
|
+
@meta = options[:meta]
|
26
|
+
|
27
|
+
# TODO: Warn about unrecognized options
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :exception, :status
|
31
|
+
alias_method :status_code, :status
|
32
|
+
|
33
|
+
def error_code
|
34
|
+
error_code_from_status
|
35
|
+
end
|
36
|
+
|
37
|
+
def title
|
38
|
+
@title || title_from_status
|
39
|
+
end
|
40
|
+
|
41
|
+
def detail
|
42
|
+
detail =
|
43
|
+
case @detail
|
44
|
+
when :exception
|
45
|
+
exception.message
|
46
|
+
when Proc
|
47
|
+
@detail.call(exception)
|
48
|
+
else
|
49
|
+
@detail.try(:to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
detail.presence || default_detail_for_status
|
53
|
+
end
|
54
|
+
|
55
|
+
def meta
|
56
|
+
if @meta.is_a?(Proc)
|
57
|
+
@meta.call(exception)
|
58
|
+
else
|
59
|
+
{ }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_payload(show_details: false, traces: nil)
|
64
|
+
payload_meta = meta
|
65
|
+
|
66
|
+
if show_details
|
67
|
+
payload_meta = payload_meta.merge(
|
68
|
+
__details__: {
|
69
|
+
exception: exception.inspect,
|
70
|
+
traces: traces || [exception.backtrace]
|
71
|
+
}
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
{
|
76
|
+
errors: [
|
77
|
+
code: error_code,
|
78
|
+
status: status_code.to_s,
|
79
|
+
title: title,
|
80
|
+
detail: detail,
|
81
|
+
meta: payload_meta
|
82
|
+
]
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def formatted_response(content_type, fallback: :json, **options)
|
87
|
+
body = build_payload(**options)
|
88
|
+
|
89
|
+
# TODO: Maybe make a helper to register these types?
|
90
|
+
to_format = content_type == :jsonapi ? "to_json" : "to_#{content_type.to_sym}"
|
91
|
+
|
92
|
+
if content_type && body.respond_to?(to_format)
|
93
|
+
formatted_body = body.public_send(to_format)
|
94
|
+
format = content_type
|
95
|
+
else
|
96
|
+
if fallback == :json
|
97
|
+
formatted_body = body.to_json
|
98
|
+
format = Mime[:json]
|
99
|
+
elsif fallback == :none
|
100
|
+
return nil
|
101
|
+
else
|
102
|
+
raise ArgumentError, "unknown fallback=#{fallback}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
[status_code, formatted_body, format]
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def error_code_from_status
|
112
|
+
code_to_symbol = Rack::Utils::SYMBOL_TO_STATUS_CODE.invert
|
113
|
+
code_to_symbol.fetch(status_code, code_to_symbol[500])
|
114
|
+
end
|
115
|
+
|
116
|
+
def title_from_status
|
117
|
+
Rack::Utils::HTTP_STATUS_CODES.fetch(
|
118
|
+
status_code,
|
119
|
+
Rack::Utils::HTTP_STATUS_CODES[500]
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def default_detail_for_status
|
124
|
+
if status_code >= 500
|
125
|
+
"We've notified our engineers and hope to address this issue shortly."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Builds a payload in the style of the Rails default handling for compatibility
|
131
|
+
class RailsExceptionHandler < ExceptionHandler
|
132
|
+
def build_payload(show_details: false, traces: nil)
|
133
|
+
body = {
|
134
|
+
status: status_code,
|
135
|
+
error: title
|
136
|
+
}
|
137
|
+
|
138
|
+
if show_details
|
139
|
+
body[:exception] = exception.inspect
|
140
|
+
if traces
|
141
|
+
body[:traces] = traces
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
body
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RescueRegistry
|
2
|
+
class ExceptionsApp
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
request = ActionDispatch::Request.new(env)
|
9
|
+
exception = request.get_header "action_dispatch.exception"
|
10
|
+
|
11
|
+
if RescueRegistry.handles_exception?(exception)
|
12
|
+
begin
|
13
|
+
content_type = request.formats.first
|
14
|
+
rescue Mime::Type::InvalidMimeType
|
15
|
+
content_type = Mime[:text]
|
16
|
+
end
|
17
|
+
|
18
|
+
if (response = RescueRegistry.response_for_public(content_type, exception))
|
19
|
+
status, body, format = response
|
20
|
+
|
21
|
+
if request.path_info != "/#{status}"
|
22
|
+
warn "status mismatch; path_info=#{request.path_info}; status=#{status}"
|
23
|
+
end
|
24
|
+
|
25
|
+
[status, { "Content-Type" => "#{format}; charset=#{ActionDispatch::Response.default_charset}",
|
26
|
+
"Content-Length" => body.bytesize.to_s }, [body]]
|
27
|
+
else
|
28
|
+
# If we have no response, it means we couldn't render for the content_type, use the default handler instead
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
@app.call(env)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module RescueRegistry
|
2
|
+
class Registry
|
3
|
+
attr_accessor :owner
|
4
|
+
|
5
|
+
def initialize(owner)
|
6
|
+
@owner = self
|
7
|
+
@handlers = { }
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize_dup(_other)
|
11
|
+
@handlers = @handlers.dup
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: Support a shorthand for handler
|
15
|
+
def register_exception(exception_class, handler: nil, **options)
|
16
|
+
raise ArgumentError, "#{exception_class} is not an Exception" unless exception_class <= Exception
|
17
|
+
|
18
|
+
handler ||= owner.try(:default_exception_handler)
|
19
|
+
raise ArgumentError, "handler must be provided" unless handler
|
20
|
+
|
21
|
+
status = options[:status] ||= handler.default_status
|
22
|
+
raise ArgumentError, "status must be provided" unless status
|
23
|
+
raise ArgumentError, "invalid status: #{status}" unless status.is_a?(Integer) || status == :passthrough
|
24
|
+
|
25
|
+
# TODO: Validate options here
|
26
|
+
|
27
|
+
# We assign the status here as a default when looking up by class (and not instance)
|
28
|
+
@handlers[exception_class] = [handler, options]
|
29
|
+
end
|
30
|
+
|
31
|
+
def handler_for_exception(exception)
|
32
|
+
handler_info = handler_info_for_exception(exception)
|
33
|
+
raise HandlerNotFound, "no handler found for #{exception.class}" unless handler_info
|
34
|
+
|
35
|
+
handler_class, handler_options = handler_info
|
36
|
+
handler_class.new(exception, **handler_options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def handles_exception?(exception)
|
40
|
+
handler_info_for_exception(exception).present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def status_code_for_exception(exception)
|
44
|
+
_, options = handler_info_for_exception(exception)
|
45
|
+
return unless options
|
46
|
+
|
47
|
+
# Return no code for passthrough.
|
48
|
+
# Alternatively we could handle this explicitly in the ExceptionWrapper monkeypatch.
|
49
|
+
options[:status] == :passthrough ? nil : options[:status]
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_response(content_type, exception, **options)
|
53
|
+
handler = handler_for_exception(exception)
|
54
|
+
handler.formatted_response(content_type, **options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def response_for_debugging(content_type, exception, traces: nil)
|
58
|
+
build_response(content_type, exception, show_details: true, traces: traces)
|
59
|
+
end
|
60
|
+
|
61
|
+
def response_for_public(content_type, exception)
|
62
|
+
build_response(content_type, exception, fallback: :none)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def handler_info_for_exception(exception)
|
68
|
+
exception_class =
|
69
|
+
case exception
|
70
|
+
when String
|
71
|
+
exception.safe_constantize
|
72
|
+
when Class
|
73
|
+
exception
|
74
|
+
else
|
75
|
+
exception.class
|
76
|
+
end
|
77
|
+
|
78
|
+
return unless exception_class
|
79
|
+
|
80
|
+
raise ArgumentError, "#{exception_class} is not an Exception" unless exception_class <= Exception
|
81
|
+
|
82
|
+
# Reverse so most recently defined takes precedence
|
83
|
+
registry = @handlers.to_a.reverse
|
84
|
+
|
85
|
+
# Look for an exact class, then for the superclass and so on.
|
86
|
+
# There might be a more efficient way to do this, but this is pretty readable
|
87
|
+
match_class = exception_class
|
88
|
+
loop do
|
89
|
+
if (found = registry.find { |(klass, _)| klass == match_class })
|
90
|
+
return found.last
|
91
|
+
elsif match_class == Exception
|
92
|
+
# We've exhausted our options
|
93
|
+
return nil
|
94
|
+
end
|
95
|
+
|
96
|
+
match_class = match_class.superclass
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
metadata
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rescue_registry
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Peter Wagenet
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '7.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '5.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: appraisal
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.2'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.2'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: kramdown-parser-gfm
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rouge
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.3'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.3'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec-rails
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.8'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.8'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: sqlite3
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '1.3'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.3'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: yard
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.9'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.9'
|
117
|
+
description:
|
118
|
+
email:
|
119
|
+
- peter.wagenet@gmail.com
|
120
|
+
executables: []
|
121
|
+
extensions: []
|
122
|
+
extra_rdoc_files: []
|
123
|
+
files:
|
124
|
+
- CHANGELOG.md
|
125
|
+
- MIT-LICENSE
|
126
|
+
- README.md
|
127
|
+
- Rakefile
|
128
|
+
- lib/rescue_registry.rb
|
129
|
+
- lib/rescue_registry/action_dispatch.rb
|
130
|
+
- lib/rescue_registry/controller.rb
|
131
|
+
- lib/rescue_registry/exception_handler.rb
|
132
|
+
- lib/rescue_registry/exceptions_app.rb
|
133
|
+
- lib/rescue_registry/railtie.rb
|
134
|
+
- lib/rescue_registry/registry.rb
|
135
|
+
- lib/rescue_registry/version.rb
|
136
|
+
- lib/tasks/rescue_registry_tasks.rake
|
137
|
+
homepage: https://github.com/wagenet/rescue_registry
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
metadata:
|
141
|
+
bug_tracker_uri: https://github.com/wagenet/rescue_registry/issues
|
142
|
+
changelog_uri: https://github.com/wagenet/rescue_registry/CHANGELOG.md
|
143
|
+
source_code_uri: https://github.com/wagenet/rescue_registry
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '2.4'
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
requirements: []
|
159
|
+
rubygems_version: 3.0.3
|
160
|
+
signing_key:
|
161
|
+
specification_version: 4
|
162
|
+
summary: Registry for Rails Exceptions
|
163
|
+
test_files: []
|