diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d6812439..1f22f7228e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2471](https://github.com/ruby-grape/grape/pull/2471): Fix absence of original_exception and/or backtrace even if passed in error! - [@numbata](https://github.com/numbata). * [#2478](https://github.com/ruby-grape/grape/pull/2478): Fix rescue_from with invalid response - [@ericproulx](https://github.com/ericproulx). +* [#2480](https://github.com/ruby-grape/grape/pull/2480): Fix rescue_from ValidationErrors exception - [@numbata](https://github.com/numbata). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/lib/grape/error_formatter/json.rb b/lib/grape/error_formatter/json.rb index 5359192340..f4df46e849 100644 --- a/lib/grape/error_formatter/json.rb +++ b/lib/grape/error_formatter/json.rb @@ -9,17 +9,18 @@ class << self def call(message, backtrace, options = {}, env = nil, original_exception = nil) result = wrap_message(present(message, env)) - rescue_options = options[:rescue_options] || {} - result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? - result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception + result = merge_rescue_options(result, backtrace, options, original_exception) if result.is_a?(Hash) + ::Grape::Json.dump(result) end private def wrap_message(message) - if message.is_a?(Exceptions::ValidationErrors) || message.is_a?(Hash) + if message.is_a?(Hash) message + elsif message.is_a?(Exceptions::ValidationErrors) + message.as_json else { error: ensure_utf8(message) } end @@ -30,6 +31,14 @@ def ensure_utf8(message) message.encode('UTF-8', invalid: :replace, undef: :replace) end + + def merge_rescue_options(result, backtrace, options, original_exception) + rescue_options = options[:rescue_options] || {} + result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? + result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception + + result + end end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index d12f29d76c..14e2c92579 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2743,6 +2743,57 @@ def self.call(message, _backtrace, _options, _env, _original_exception) it { is_expected.to have_key('backtrace') & have_key('original-exception') } end end + + context 'when rescue validation errors include backtrace and original exception' do + let(:app) do + response_type = response_format + + Class.new(Grape::API) do + format response_type + + rescue_from Grape::Exceptions::ValidationErrors, backtrace: true, original_exception: true do |e| + error!(e, 418, {}, e.backtrace, e) + end + + params do + requires :weather + end + get '/forecast' do + 'sunny' + end + end + end + + before do + get '/forecast' + end + + context 'with json response type format' do + subject { JSON.parse(last_response.body) } + + let(:response_format) { :json } + + it 'does not include backtrace or original exception' do + expect(subject).to match([{ 'messages' => ['is missing'], 'params' => ['weather'] }]) + end + end + + context 'with txt response type format' do + subject { last_response.body } + + let(:response_format) { :txt } + + it { is_expected.to include('backtrace', 'original exception') } + end + + context 'with xml response type format' do + subject { Grape::Xml.parse(last_response.body)['error'] } + + let(:response_format) { :xml } + + it { is_expected.to have_key('backtrace') & have_key('original-exception') } + end + end end describe '.content_type' do