Tuesday, September 27, 2011

Rails exception handling - Egregious

Version 0.2.9 released to https://rubygems.org/gems/egregious on 10.23.2015
        Added support for exceptions that define a http_status. The exception map can override this.
        This is a good way to allow a raise to specify a http_status using a custom exception.
        The idea for this came from the Stripe::Error exception classes.
        Also updated Gemfile.lock to ruby 2.2.1 and latest dependencies. This is for specs only.

Egregious is a rails based exception handling gem for well defined http exception handling for json, xml and html.

If you have a json or xml api into your rails application, you probably have added your own exception handling to map exceptions to a http status and formatting your json and xml output.

You probably have code sprinkled about like this:
rescue_from CanCan::AccessDenied do |exception|
    flash[:alert] = exception.message
    respond_to do |format|
      format.html { redirect_to dashboard_path }
      format.xml { render :xml => exception.to_xml, :status => :forbidden }
      format.json { render :json=> exception.to_json, :status => :forbidden }
    end
  end

This example is straight from the CanCan docs. You'll notice a couple of things here. This handles the CanCan::AccessDenied exception only. It then will redirect to the startup page, or render xml and json returning the http status code of :forbidden (403). You can see one of the first features of the Egregious gem. We extend Exception to add the to_xml and to_json methods. These return a well structured error that can be consumed by the API client.
Exception.new("Hi Mom").to_xml

returns:
"Hi MomException"

Exception.new("Hi Dad").to_json

returns:
"{\"error\":\"Hi Dad\", \"type\":\"Exception\"}"

So that's pretty handy in itself. Now all exceptions have a json and xml api that describe them. It happens to be the same xml and json that is returned from the errors active record object, with the addition of the type element. That allows you to mix and match validations and exceptions. Wow, big deal. We'll it is. If you are writing a client then you need to have a very well defined error handling. I'd like to see all of rails do this by default. So that anyone interacting with a rails resource has a consistent error handling experience. (Expect more on being a good REST API in future posts.) As a client we can now handle errors in a consistent way.

Besides the error message we would like a well defined mapping of classes of exceptions to http status codes. The idea is that if I get back a specific http status code then I can program against that 'class' of problems. For example if I know that what I did was because of invalid input from my user, I can display that message back to the user. They can correct it and continue down the path. But if the Http status code says that it was a problem with the server, then I know that I need to log it and notify someone to see how to resolve it.

We handle all exceptions of a given class with a mapping to an http status code. With all the most common Ruby, Rails, Devise, Warden and CanCan exceptions having reasonable defaults. (Devise, Warden and CanCan are all optional and ignored if their gems are not installed.)

As of 0.2.9 you can also define a method named 'http_status' on the exception and it will be used as the status code. This is a nice pattern that allows you to raise an exception and specify the status code. The Egregious::Error allows you to do this as a second parameter to initialize:
  raise Egregious::Error.new("My very bad error", :payment_required)

If the problem was the api caller then the result codes are in the 300 range. If the problem was on the server then the status codes are in the 500 range.

I'm guessing if you bother to read this far, you are probably interested in using Egregious. Its simple to use and configure. To install:

In you Gemfile add the following:
gem 'egregious'


In your ApplicationController class add the following at or near the top:
class ApplicationController < ActionController::Base
  include Egregious
  protect_from_forgery
end


That's it. You will now get reasonable api error handling.

If you want to add your own exceptions to http status codes mappings, or change the defaults add an initializer and put the following into it:
Egregious.exception_codes.merge!({NameError => :bad_request})

Here you can re-map anything and you can add new mappings.

Note: If you think the default exception mappings should be different, please contact me via the Egregious github project.

We also created exceptions for each of the http status codes, so that you can throw those exceptions in your code. Its an easy way to throw the right status code and setup a good message for it. If you want to provide more context, you can derive you own exceptions and add mappings for them.

Here is an example of throwing a bad request exception:
raise Egregious::BadRequest.new("You can not created an order without a customer.") unless customer_id


Egregious adds mapping of many exceptions, if you have your own rescue_from handlers those will get invoked. You will not lose any existing behavior, but you also might not see the changes you expect until you remove or modify those rescue_from calls. At a minimum I suggest using the .to_xml and .to_json calls io your existing rescue_from methods/blocks.

And finally if you don't like the default behavior. You can override any portion of it and change it to meet your needs.

If you want to change the behavior then you can override the following methods in your ApplicationController.
# override this if you want your flash to behave differently
def egregious_flash(exception)
    flash.now[:alert] = exception.message
end


# override this if you want your logging to behave differently
def egregious_log(exception)
    logger.fatal(
        "\n\n" + exception.class.to_s + ' (' + exception.message.to_s + '):\n    ' +
            clean_backtrace(exception).join("\n    ") +
            "\n\n")
    HoptoadNotifier.notify(exception) if defined?(HoptoadNotifier)
end


# override this if you want to change your respond_to behavior
def egregious_respond_to(exception)
    respond_to do |format|
          status = status_code_for_exception(exception)
          format.xml { render :xml=> exception.to_xml, :status => status }
          format.json { render :json=> exception.to_json, :status => status }
          # render the html page for the status we are returning it exists...if not then render the 500.html page.
          format.html { render :file => File.exists?(build_html_file_path(status)) ?
                                          build_html_file_path(status) : build_html_file_path('500')}
    end
end


# override this if you want to change what html static file gets returned.
def build_html_file_path(status)
    File.expand_path(Rails.root, 'public', status + '.html')
end


# override this if you want to control what gets sent to airbrake
# optionally you can configure the airbrake ignore list
def notify_airbrake(exception)
    # for ancient clients - can probably remove
    HoptoadNotifier.notify(exception) if defined?(HoptoadNotifier)
    # tested with airbrake 3.1.15 and 4.2.1
    env['airbrake.error_id'] = Airbrake.notify_or_ignore(exception) if defined?(Airbrake)
end


We are using this gem in all our Rails projects.

Go forth and be egregious!

No comments:

Post a Comment