Rails Exception Handler Build Status

This is an exception handler for Rails 3 built as Rack middleware. It enables you to save the key information from the error message in a database somewhere and display a customized error message to the user within the applications layout file. You can hook this gem into all your rails apps and gather the exception reports in one place. If you make yourself a simple web front on top of that you have a user friendly way of keeping track of exceptions raised by your rails apps.

Compatiblity

The gem is tested against Rails 3.0.9. It does not work with Rails 2. See travis-ci for info on which rubies it is tested against: http://travis-ci.org/#!/Sharagoz/rails_exception_handler

Installation

Add this line to your Gemfile:

gem 'rails_exception_handler', '~> 1.0'

Create an initializer in config/initializers called rails_exception_handler.rb and uncomment the options where you want something other than the default:

RailsExceptionHandler.configure do |config|
  # config.environments = [:development, :test, :production]                # Defaults to [:production]
  # config.storage_strategies = [:active_record, :rails_log, :remote_url => {:target => 'http://example.com'}] # Defaults to []
  # config.fallback_layout = 'home'                                         # Defaults to 'application'
  # config.store_user_info = {:method => :current_user, :field => :login}   # Defaults to false
  # config.filters = [                                                      # No filters are  enabled by default
  #   :all_404s,
  #   :no_referer_404s,
  #   {:user_agent_regxp => /\b(ApptusBot|TurnitinBot|DotBot|SiteBot)\b/i},
  #   {:target_url_regxp => /\b(myphpadmin)\b/i}
  # ]
  # config.responses = {                                                    # There must be a default response. The rest is up to you.
  #   :default => "<h1>500</h1><p>Internal server error</p>",
  #   :custom => "<h1>404</h1><p>Page not found</p>"
  # }
  # config.response_mapping = {                                             # All errors are mapped to the :default response unless overridden here
  #  'ActiveRecord::RecordNotFound' => :custom,
  #  'ActionController::RoutingError' => :custom,
  #  'AbstractController::ActionNotFound' => :custom
  # }
end

Configuration options explained

environments

An array of symbols that says which Rails environments you want the exception handler to run in.

config.environments = [:production, :test, :development]

Default value: [:production]

storage_strategies

An array of zero or more symbols that says which storage strategies you want to use. Each are explained in detail in separate sections at the end of this document.

config.storage_strategies = [:active_record, :rails_log, :remote_url => {:target => 'http://example.com'}]

Default value: []

fallback_layout

config.fallback_layout = 'home'

Default value: 'application'

The exception handler will always use the layout file of the controller action that was accessed when the error occured. However, when routing errors occures there are no controller action to get this layout from, so it falls back to the default 'application' layout that most apps have. If your application does not have a layout file called 'application', then you need to override this, otherwise a "missing layout" exception will occur.

store_user_info

Having some way of identifying the user can be very useful at times, so I always store information on who generated the exception in applications that have a log in feature. To enable this you need to specify the name of the controller method that provides the user object and the name of the field which contains the info you want to save.

config. = {:method => :current_user, :field => :login}

Default value: false (no info will be stored) If you turn this on and the error is generated by a client that is not logged in, then "Anonymous" will be used.

responses and response_mapping

Create a set of responses and then map specific exceptions to these responses. There needs to be a response called :default which is used for the exceptions that are not explicity mapped to a response.

config.responses = {
  :default => "<h1>500</h1><p>Internal server error</p>",
  :not_found => "<h1>404</h1><p>Page not found</p>",
  :wrong_token => "<h1>500</h1><p>There was a problem authenticating the submitted form. Reload the page and try again.</p>",
  :teapot => "<h1>418</h1><p>I'm a teapot</p>"
}
config.response_mapping = {
 'ActiveRecord::RecordNotFound' => :not_found,
 'ActionController:RoutingError' => :not_found,
 'AbstractController::ActionNotFound' => :not_found,
 'ActionController::InvalidAuthenticityToken' => :wrong_token,
 'Teapot::CoffeeGroundsNotSupported' => :teapot
}

filters

All filters are disabled by default. I recommend you deploy your application this way, and then add filters as they become necessary. The only reason I've ever wanted filtering have been due to what seem like poorly programmed web crawlers and black bots probing for security holes. Every once in a while I'll get dozens of errors within a few minutes caused by a bot looking for things like Joomla/Wordpress libraries with known security holes, or a web crawler that follows the target of forms.

:all_404s

config.filters = [:all_404s]

When turned on the following exceptions will no longer be stored: ActionController::RoutingError, AbstractController::ActionNotFound, ActiveRecord::RecordNotFound Consider this a last resort. You will miss all "real" 404s when this is turned on, like broken redirections.

:no_referer_404s

config.filters = [:no_referer_404s]

ActionController::RoutingError, AbstractController::ActionNotFound, ActiveRecord::RecordNotFound will be ignored if it was caused by a request without a referer. This is very effective against bots. 99.9% of the time a routing error with no referer will be caused by a bot, and then once in a while it will be caused by a real user that happened to generate an error on the first page he opened. You will get a lot less false positives with this filter than :all_404s.

:user_agent_regxp

The legit bots usually adds something to the user agent string that lets you identify them. You can use this to filter out the errors they genereate, and be pretty sure you are not going to get any false positives. The regular expression I use looks something like this:

config.filters = [:user_agent_regxp => /\b(ZyBorg|Yandex|Jyxobot|Huaweisymantecspider|ApptusBot|TurnitinBot|DotBot)\b/i]

If you (like me) dont know regular expressions by heart, then http://www.rubular.com/ is great tool to use when creating a regxp.

:target_url_regxp

Sometimes the bad bots add a common user agent string and a referer to their requests, which makes it hard to filter them without filtering all routing errors. I guess they do this to make it look less suspicious. What you can often do is to filter on what they target, which is usually security holes in some well known library like phpMyAdmin.

config.filters = [:target_url_regxp => /\b(phpMyAdmin|joomla|wordpress)\b/i]

Storage strategy - active record

config.storage_strategies = [:active_record]

This means that the error reports will be stored through active record directly to a database, which is pretty much the whole reason why I created this library in the first place. A new entry called exception_database is needed in database.yml:

# for mysql the entry would look something like this:
exception_database:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: your_database
  pool: 5
  username: user
  password: secret
  host: 127.0.0.1

You could of course store the error messages in the same database as the application uses, but one of the main purposes of this library is to enable you to easily store error reports from many applications in the same database, so I recommend you set up a separate dedicated database for this.

The exception database needs a table called error_messages. Here's a migration script that you can use to create the table with the necessary fields:

class CreateErrorMessages < ActiveRecord::Migration
  def self.up
    create_table :error_messages do |t|
      t.text :class_name
      t.text :message
      t.text :trace
      t.text :params
      t.text :target_url
      t.text :referer_url
      t.text :user_agent
      t.string :user_info
      t.string :app_name

      t.timestamps
    end
  end

  def self.down
    drop_table :error_messages
  end
end

Storage strategy - rails log

config.storage_strategies = [:rails_log]

An error will be logged in the standard rails log. The log i located in the RAILS_ROOT/log directory and is named after the Rails environment. Example of what a report might look like:

TARGET:     http://example.com/users
REFERER:    http://example.com/
PARAMS:     {"controller"=>"users", "action"=>"index"}
USER_AGENT: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30
USER_INFO:  matz
ActionView::Template::Error (ActionView::Template::Error):
activesupport (3.0.7) lib/active_support/whiny_nil.rb:48:in `method_missing'
actionpack (3.0.7) lib/action_view/template.rb:135:in `block in render'
activesupport (3.0.7) lib/active_support/notifications.rb:54:in `instrument'
(the rest of the stack trace has been omitted from the example)

Storage strategy - remote url

config.storage_strategies = [:remote_url => {:target => 'http://example.com/error_messages'}]

This option is meant for those who want to store the exception in a database table, but does not have direct access to the database itself, making active record store unsuitable. You need a web app on a server that has access to the database. An HTTP POST request will be sent to the specified URL with the error message as data. If you use a Rails app at the other end you should simply be able to do ErrorMessage.create(params[:error_message]) to save the report.