Skip to content

Commit 9b5d171

Browse files
committed
Merge pull request #290 from togglepro/exception_whitelist
Configure exception class whitelist to be reraised by operations processors.
2 parents 5773f49 + a416828 commit 9b5d171

7 files changed

Lines changed: 72 additions & 9 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ end
272272
```ruby
273273
class BookResource < JSONAPI::Resource
274274

275-
# Only book_admins may see unapproved comments for a book. Using
275+
# Only book_admins may see unapproved comments for a book. Using
276276
# a lambda to select the correct relation on the model
277277
has_many :book_comments, relation_name: -> (options = {}) {
278278
context = options[:context]
@@ -1240,6 +1240,14 @@ JSONAPI.configure do |config|
12401240
config.top_level_meta_record_count_key = :record_count
12411241
12421242
config.use_text_errors = false
1243+
1244+
# List of classes that should not be rescued by the operations processor.
1245+
# For example, if you use Pundit for authorization, you might
1246+
# raise a Pundit::NotAuthorizedError at some point during operations
1247+
# processing. If you want to use Rails' `rescue_from` macro to
1248+
# catch this error and render a 403 status code, you should add
1249+
# the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.
1250+
config.exception_class_whitelist = []
12431251
end
12441252
```
12451253

lib/jsonapi/active_record_operations_processor.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ def process_operation(operation)
2929
raise e
3030

3131
rescue => e
32-
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
33-
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
34-
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
32+
if JSONAPI.configuration.exception_class_whitelist.include?(e.class)
33+
raise e
34+
else
35+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
36+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
37+
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
38+
end
3539
end
3640
end

lib/jsonapi/configuration.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class Configuration
1616
:use_text_errors,
1717
:top_level_links_include_pagination,
1818
:top_level_meta_include_record_count,
19-
:top_level_meta_record_count_key
19+
:top_level_meta_record_count_key,
20+
:exception_class_whitelist
2021

2122
def initialize
2223
#:underscored_key, :camelized_key, :dasherized_key, or custom
@@ -45,6 +46,14 @@ def initialize
4546
self.top_level_meta_record_count_key = :record_count
4647

4748
self.use_text_errors = false
49+
50+
# List of classes that should not be rescued by the operations processor.
51+
# For example, if you use Pundit for authorization, you might
52+
# raise a Pundit::NotAuthorizedError at some point during operations
53+
# processing. If you want to use Rails' `rescue_from` macro to
54+
# catch this error and render a 403 status code, you should add
55+
# the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.
56+
self.exception_class_whitelist = []
4857
end
4958

5059
def json_key_format=(format)
@@ -77,6 +86,8 @@ def operations_processor=(operations_processor)
7786
attr_writer :top_level_meta_include_record_count
7887

7988
attr_writer :top_level_meta_record_count_key
89+
90+
attr_writer :exception_class_whitelist
8091
end
8192

8293
class << self

lib/jsonapi/operations_processor.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,13 @@ def process_operation(operation)
8989

9090
rescue => e
9191
# :nocov:
92-
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
93-
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
94-
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
92+
if JSONAPI.configuration.exception_class_whitelist.include?(e.class)
93+
raise e
94+
else
95+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
96+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
97+
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
98+
end
9599
# :nocov:
96100
end
97101
end

test/controllers/controller_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ def test_index
1111
assert json_response['data'].is_a?(Array)
1212
end
1313

14+
def test_exception_class_whitelist
15+
original_config = JSONAPI.configuration.dup
16+
JSONAPI.configuration.operations_processor = :error_raising
17+
# test that the operations processor rescues the error when it
18+
# has not been added to the exception_class_whitelist
19+
get :index
20+
assert_response 500
21+
# test that the operations processor does not rescue the error when it
22+
# has been added to the exception_class_whitelist
23+
JSONAPI.configuration.exception_class_whitelist << PostsController::SpecialError
24+
get :index
25+
assert_response 403
26+
ensure
27+
JSONAPI.configuration = original_config
28+
end
29+
1430
def test_index_filter_with_empty_result
1531
get :index, {filter: {title: 'post that does not exist'}}
1632
assert_response :success

test/fixtures/active_record.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,19 @@ class CountingActiveRecordOperationsProcessor < ActiveRecordOperationsProcessor
407407
end
408408
end
409409

410+
# This processor swaps in a mock for the operation that will raise an exception
411+
# when it receives the :apply method. This is used to test the
412+
# exception_class_whitelist configuration.
413+
class ErrorRaisingOperationsProcessor < ActiveRecordOperationsProcessor
414+
def process_operation(operation)
415+
mock_operation = Minitest::Mock.new
416+
mock_operation.expect(:apply, true) do
417+
raise PostsController::SpecialError
418+
end
419+
super(mock_operation)
420+
end
421+
end
422+
410423
### CONTROLLERS
411424
class AuthorsController < JSONAPI::ResourceController
412425
end
@@ -416,6 +429,13 @@ class PeopleController < JSONAPI::ResourceController
416429

417430
class PostsController < ActionController::Base
418431
include JSONAPI::ActsAsResourceController
432+
class SpecialError < StandardError; end
433+
434+
# This is used to test that classes that are whitelisted are reraised by
435+
# the operations processor.
436+
rescue_from PostsController::SpecialError do
437+
head :forbidden
438+
end
419439
end
420440

421441
class CommentsController < JSONAPI::ResourceController

test/test_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
require 'rails/all'
1414
require 'rails/test_help'
15+
require 'minitest/mock'
1516
require 'jsonapi-resources'
1617

1718
require File.expand_path('../helpers/value_matchers', __FILE__)
@@ -260,4 +261,3 @@ def unformat(value)
260261
end
261262
end
262263
end
263-

0 commit comments

Comments
 (0)