Following on from our post on Dynamic Error Page in Rails, this week we're going to look at raising and rescuing custom errors in a Rails application.
It's often useful to map custom Ruby errors to HTTP response status codes and have Rails render the appropriate HTML error pages. For example, you might have a controller that is acting as a simple proxy to a third party service such as Twitter or Facebook, and you need any of the HTTP errors encountered when calling those sites to be handled natively by your app. Another use case would be in a Service-oriented architecture (SOA), where you want any errors in your back end services propagated to your front end web application.
In this post we'll demonstrate rescuing status errors in an imaginary proxy controller using the awesome Faraday gem. For the sake of brevity we've omitted the inclusion of tests though in the wild we'd build such a feature using TDD and our favourite test weapon, RSpec.
Not Found
To start, let's handle basic 404 Not Found errors that occur when calling a service. For this we'll need a custom error class that extends StandardError.
# lib/errors/not_found.rb
module Errors
class NotFound < StandardError; end
end
Faraday provides a neat Rack-esque middleware feature. By creating our own custom middleware we can catch any Faraday 404s and raise our custom error. Furthermore, we can re-use the middleware anytime we need the same behaviour.
# lib/errors/raise_error.rb
module Errors
class RaiseError < Faraday::Response::Middleware
def on_complete(env)
raise Errors::NotFound if env[:status] == 404
end
end
end
Now for the proxy controller.
# app/controllers/proxy_controller.rb
class ProxyController < ApplicationController
def index
connection = Faraday.new(:url => 'http://someservice') do |f|
f.adapter Faraday.default_adapter
f.use Errors::RaiseError # Include custom middleware
end
response = connection.get('/some/resource')
render :text => response.body
end
end
At this point any NotFounds raised will still result in a 500 Internal Server Error in Rails. To alleviate this let's create a module that uses rescue_from, catches any custom NotFounds and renders the default 404 page.
# lib/errors/rescue_error.rb
module Errors
module RescueError
def self.included(base)
base.rescue_from Errors::NotFound do |e|
render "public/404", :status => 404
end
end
end
end
We can then mixin RescueError into our application controller and handle NotFounds app-wide.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Errors::RescueError
end
Unprocessible Entity and Internal Server Error
Next, let's create custom errors to help us manage proxy 422s and 500s.
# lib/errors/internal_server_error.rb
module Errors
class InternalServerError < StandardError; end
end
# lib/errors/unprocessable_entity.rb
module Errors
class UnprocessableEntity < StandardError; end
end
We'll also need to modify the RaiseError middleware to raise those errors. Here, we also ignore any non-error response codes, and treat any unknown error responses as 500s.
# lib/errors/raise_error.rb
module Errors
class RaiseError < Faraday::Response::Middleware
def on_complete(env)
# Ignore any non-error response codes
return if (status = env[:status]) < 400
case status
when 404
raise Errors::NotFound
when 422
raise Errors::UnprocessableEntity
else
raise Errors::InternalServerError # Treat any other errors as 500
end
end
end
end
Now, let's change the RescueError implementation to render the appropriate HTML page.
# lib/errors/rescue_error.rb
module Errors
module RescueError
def self.included(base)
base.rescue_from Errors::NotFound do |e|
render "public/404", :status => 404
end
base.rescue_from Errors::UnprocessableEntity do |e|
render "public/422", :status => 422
end
base.rescue_from Errors::InternalServerError do |e|
render "public/500", :status => 500
end
end
end
end
Refactoring
So far so good. The code does what we need but there's far too much duplication. Furthermore, if we want to map additional error codes we'll have to add more branches to the switch statement in RaiseError and more rescue_from handlers to the RescueError class.
Accordingly, it's time to refactor the existing code. Firstly, let's delete our existing StandardError subclasses, and create the equivalent classes using a sprinkling of metaprogramming.
# lib/errors.rb
module Errors
# Error constants
NOT_FOUND = 404
UNPROCESSABLE_ENTITY = 422
INTERNAL_SERVER_ERROR = 500
# Base subclass for all response errors
class ResponseError < StandardError; end
class << self
# Returns a hash of error names to response codes.
def error_constants
self.constants.each_with_object({}) do |name, hash|
# Ignore any class constants
next if (code = Errors.const_get(name)).is_a?(Class)
hash[name] = code
end
end
# Returns a class name from a constant name.
def class_name_for_error_name(name)
name.to_s.titleize.gsub(' ', '')
end
# Returns the error class for a given constant name.
# Default to InternalServerError.
def class_for_error_name(name)
class_name = class_name_for_error_name(name)
const_defined?(class_name) ? Errors.const_get(class_name) : Errors::InternalServerError
end
# Returns the error class for a given error code.
# Default to InternalServerError.
def class_for_error_code(code)
name = error_constants.select { |k, v| v == code }.keys.first
name.present? ? class_for_error_name(name) : Errors::InternalServerError
end
end
end
# Dynamically creates a subclass of ResponseError for each error constant.
# Adds a code method to return the correct response code.
# Adds the new class to the constants in the Errors module.
Errors.error_constants.each do |name, code|
klass = Class.new(Errors::ResponseError)
klass.send(:define_method, :code) { code }
Errors.const_set(Errors.class_name_for_error_name(name), klass)
end
This is a large refactoring, but permits us to vastly simplify the error raising code.
# lib/errors/raise_error.rb
module Errors
class RaiseError < Faraday::Response::Middleware
def on_complete(env)
return if (status = env[:status]) < 400
raise Errors.class_for_error_code(status)
end
end
end
And similarly, the error rescuing code.
# lib/errors/rescue_error.rb
module Errors
module RescueError
def self.included(base)
base.rescue_from Errors::ResponseError do |e|
render "public/#{e.code}", :status => e.code
end
end
end
end
Now, raising and rescuing additional error types is as simple as adding new constants to the errors.rb file.
# lib/errors/rescue_error.rb
module Errors
# Error constants
BAD_REQUEST = 400
NOT_AUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
METHOD_NOT_ALLOWED = 405
BAD_FORMAT = 406
UNPROCESSABLE_ENTITY = 422
INTERNAL_SERVER_ERROR = 500
...
end
Summary
This brings our recent posts on error handling in Rails to a close. Feel free to add any suggestions, questions or feedback in the comments section.