Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bubble up to the error_formatter the original exception and the backtrace #1652

Merged
merged 8 commits into from
Jul 12, 2017

Conversation

dcsg
Copy link
Contributor

@dcsg dcsg commented Jul 4, 2017

When a custom parser is not able to parse the json input body, it raises an exception that is rescued in the Grape::Middleware::Formatter:111. However, the throw statement does not bubble up the root cause by not setting the backtrace. This makes it harder for the error_formatter to understand what caused the exception and to properly handle it.

I tried to add tests to this but was not that easy, any suggestions?

Before the fix

grape/middleware/formatter.rb:

image

my_custom_error_formatter.rb:

image

After the fix

my_custom_error_formatter.rb:

image

@@ -110,7 +110,7 @@ def read_rack_input(body)
rescue Grape::Exceptions::Base => e
raise e
rescue StandardError => e
throw :error, status: 400, message: e.message
throw :error, status: 400, message: e.message, backtrace: e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a standard pattern somehow in Ruby? I mean I would expect backtrace to be an actual backtrace and not the original exception. Is anyone doing it like this elsewhere?

Are there other places we throw :error like this that also need fixing?

Copy link
Contributor Author

@dcsg dcsg Jul 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a standard pattern somehow in Ruby? I mean I would expect backtrace to be an actual backtrace and not the original exception. Is anyone doing it like this elsewhere?

I am quite new in the Ruby world, so TBH I don't know if this is a standard pattern but I would say no. However the Grape library as a "contract" for the formatter: formatter.call(message, backtrace, options, env).

      # lib/grape/middleware/error.rb:90
      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] || []
        rack_response(format_message(message, backtrace), status, headers)
      end

      # lib/grape/middleware/error.rb:103
      def format_message(message, backtrace)
        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." unless formatter
        formatter.call(message, backtrace, options, env)
      end

This method is called here:

      # lib/grape/middleware/error.rb:29
      def call!(env)
        @env = env

        begin
          error_response(catch(:error) do
            return @app.call(@env)
          end)
        rescue StandardError => e
           # ...
        end

Are there other places we throw :error like this that also need fixing?

Probably yes, but I didn't investigate any other use cases. This one affected us.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it works, backtrace is meant to be a backtrace. So I think either adding an original exception into the contract or passing the actual backtrace is the right thing to do.

Copy link
Contributor Author

@dcsg dcsg Jul 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do both, what would be your advice on how to test it?

@dblock
Copy link
Member

dblock commented Jul 5, 2017

Danger is right, this also needs tests.

@dcsg dcsg force-pushed the bubble-up-exception branch from 9db6988 to b6f30b1 Compare July 5, 2017 23:55
@dcsg
Copy link
Contributor Author

dcsg commented Jul 5, 2017

@dblock would you care to review the code again? I already did the changes you requested (added both backtrace and original exception) and added a test.

I will update the changelog as soon as I have some feedback regarding the implementation.

@dcsg dcsg changed the title Bubble up to the error_formatter the root exception Bubble up to the error_formatter the root exception and the backtrace Jul 6, 2017
@ruby-grape ruby-grape deleted a comment from dcsg Jul 6, 2017
@dblock
Copy link
Member

dblock commented Jul 6, 2017

Thanks for hanging in here @dcsg.

I am not loving this at all :( I think we're going the wrong way. The following passes all kinds of things here which are all properties of e.

throw :error, status: 400, message: e.message, backtrace: e.backtrace, exception: e

I think what we want to write is this:

throw :error, status: 400, original_exception: e

Does this look feasible? It might mean breaking some backwards compat which I am OK with, but maybe we don't have to?

@dcsg
Copy link
Contributor Author

dcsg commented Jul 6, 2017

@dblock did the changes, what do you think about them?

@@ -92,19 +95,24 @@ def error_response(error = {})
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] || []
rack_response(format_message(message, backtrace), status, headers)
backtrace = error[:backtrace] || error[:original_exception] && error[:original_exception].backtrace || []
Copy link
Contributor Author

@dcsg dcsg Jul 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dblock here I could replace this with the following:

backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []

However, rubocop complains because of Ruby 2.1.*. Does Grape still support that Ruby version?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grape supports whatever is in https://github.com/ruby-grape/grape/blob/master/.travis.yml, so 2.2+, so we can tame Rubocop accordingly. Since we don't use &. anywhere else maybe just expanding it is better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine.

Copy link
Member

@dblock dblock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some minor comments left and I am OK merging this because it does what it does. However I'd like us to go further and get rid of passing backtrace through, maybe in a future PR?

Basically I want

throw :error, status: 400, message: e.message, backtrace: e.backtrace, exception: e

to become

throw :error, status: 400, original_exception: e

@@ -4,12 +4,15 @@ module Json
extend Base

class << self
def call(message, backtrace, options = {}, env = nil)
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
result = wrap_message(present(message, env))

if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if (options[:rescue_options] || {}) is dup, so unwrap it.

rescue_options = options[:rescue_options]
if (rescue_options)
   if ...  
end

@@ -77,13 +80,13 @@ def exec_handler(e, &handler)
end
end

def error!(message, status = options[:default_status], headers = {}, backtrace = [])
def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = '')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here original_exception should be defaulted to nil.

@@ -92,19 +95,24 @@ def error_response(error = {})
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] || []
rack_response(format_message(message, backtrace), status, headers)
backtrace = error[:backtrace] || error[:original_exception] && error[:original_exception].backtrace || []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine.

@@ -45,7 +45,7 @@ def build_formatted_response(status, headers, bodies)
Rack::Response.new(bodymap, status, headers)
end
rescue Grape::Exceptions::InvalidFormatter => e
throw :error, status: 500, message: e.message
throw :error, status: 500, message: e.message, backtrace: e.backtrace, exception: e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sholdn't this be original_exception: e?

CHANGELOG.md Outdated
@@ -6,7 +6,7 @@

#### Fixes

* Your contribution here.
* [#1652](https://github.com/ruby-grape/grape/pull/1652): Bubble up to the error_formatter the root exception - [@dcsg](https://github.com/dcsg).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

root -> original

@dcsg
Copy link
Contributor Author

dcsg commented Jul 7, 2017

I have some minor comments left and I am OK merging this because it does what it does. However I'd like us to go further and get rid of passing backtrace through, maybe in a future PR?

Doing this change it will break backwards compatibility. Do you want to do it now?

@dcsg
Copy link
Contributor Author

dcsg commented Jul 7, 2017

@dblock did the changes.

@dcsg dcsg changed the title Bubble up to the error_formatter the root exception and the backtrace Bubble up to the error_formatter the original exception and the backtrace Jul 7, 2017
@@ -110,7 +110,7 @@ def read_rack_input(body)
rescue Grape::Exceptions::Base => e
raise e
rescue StandardError => e
throw :error, status: 400, message: e.message
throw :error, status: 400, message: e.message, backtrace: e.backtrace, exception: e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still exception: ..., which tells me there's no tests for it cause this doesn't work.

Copy link
Contributor Author

@dcsg dcsg Jul 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't find a way to test these use cases. Do you have any suggestion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a trivial custom parser that raises an exception, it will throw on parser.call and hit this code.

CHANGELOG.md Outdated
@@ -6,7 +6,7 @@

#### Fixes

* Your contribution here.
* [#1652](https://github.com/ruby-grape/grape/pull/1652): Bubble up to the error_formatter the original exception - [@dcsg](https://github.com/dcsg).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and this is a feature, not a fix, right? Should be above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO is both a new feature (the original_exception) and a fix (the backtrace).

@dcsg dcsg force-pushed the bubble-up-exception branch 5 times, most recently from e850d37 to 261de9e Compare July 7, 2017 20:49
@dcsg
Copy link
Contributor Author

dcsg commented Jul 11, 2017

add to run rubocop --auto-gen-config because of this:

spec/grape/middleware/formatter_spec.rb:330:19: C: Avoid using {...} for multi-line blocks.
    let(:options) {
                  ^
spec/grape/middleware/formatter_spec.rb:339:29: C: Avoid using {...} for multi-line blocks.
      error = catch(:error) {
                            ^

@dcsg
Copy link
Contributor Author

dcsg commented Jul 11, 2017

@dblock added the missing test!

@dcsg dcsg force-pushed the bubble-up-exception branch from 6b08b4f to 4521544 Compare July 11, 2017 00:17
@dcsg
Copy link
Contributor Author

dcsg commented Jul 11, 2017

quite odd the tests that are failing. Any ideas? I didn't change at all anything related to those tests that are failing in the past 2 commits

@dcsg dcsg force-pushed the bubble-up-exception branch from ef0c192 to 264aee1 Compare July 11, 2017 00:49
@dblock
Copy link
Member

dblock commented Jul 11, 2017

Found the problem with the build, #1655. I'll rebase/merge this one next.

@dblock
Copy link
Member

dblock commented Jul 11, 2017

You can rebase on master, otherwise I'll get to this soon.

dcsg added 3 commits July 11, 2017 20:53
…rser is not able to parse a json input body.
Add backtrace in missing `throw :error`
Add tests
@dcsg dcsg force-pushed the bubble-up-exception branch from 264aee1 to 469b05d Compare July 11, 2017 19:54
@dcsg
Copy link
Contributor Author

dcsg commented Jul 11, 2017

@dblock rebase done

@dblock dblock merged commit 3c0be5e into ruby-grape:master Jul 12, 2017
@dblock
Copy link
Member

dblock commented Jul 12, 2017

Merged, thank you.

@dblock
Copy link
Member

dblock commented Jul 12, 2017

Want to try to make the backward (in)compatible change to reduce the number of things we pass into the raise on top of this?

@dcsg
Copy link
Contributor Author

dcsg commented Jul 12, 2017

@dblock yes, I can do it

@dcsg dcsg deleted the bubble-up-exception branch July 12, 2017 15:39
@dcsg
Copy link
Contributor Author

dcsg commented Jul 12, 2017

BTW when do you expect to release a new tag with this fix?

@dcsg
Copy link
Contributor Author

dcsg commented Jul 13, 2017

@dblock I guess it will be kind of hard or almost impossible to do the refactor you want.

# lib/grape/dsl/inside_route.rb:103

      # End the request and display an error to the
      # end user with the specified message.
      #
      # @param message [String] The message to display.
      # @param status [Integer] the HTTP Status Code. Defaults to default_error_status, 500 if not set.
      def error!(message, status = nil, headers = nil)
        self.status(status || namespace_inheritable(:default_error_status))
        throw :error, message: message, status: self.status, headers: headers
      end

What do you think?

@dblock
Copy link
Member

dblock commented Jul 14, 2017

We'll do a release soon.

Give the refactor a shot and lets see if we can come up with something!

vrinek added a commit to vrinek/grape that referenced this pull request Sep 27, 2017
Updates README according to ruby-grape#1652

Prevents `ArgumentError: wrong number of arguments (given 5, expected 4)` from happening when following the README.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants