Returning Multiple Formats with Rails Dynamic Error Pages
Yesterday my coworker Eric asked me to build out some dynamic error pages for the upcoming
version of one of our apps. He suggested a certain pattern that we used in a previous application.
Being the adventurous soul that I am, I wanted to make sure his way was the best way. After some
deep soul google searching, I found out that Eric’s method was considered best practice. It’s
described in some detail on José Valim’s blog post, but I’ll break
it down for you here:
First, we need a controller for our error pages. Start out with something simple, like this:
# app/controllers/errors_controller.rb
class ErrorsController < ApplicationController
layout 'error' # only if you want a separate layout for your errors
# 404 Not Found
def not_found
end
# 422 Unprocessable Entity
def unprocessable
end
# 500 Internal Server Error
def internal_server
end
end
Now let’s add some routes:
YourApp::Application.routes.draw do
match '/404', to: 'errors#not_found'
match '/422', to: 'errors#unprocessable'
match '/500', to: 'errors#internal_server'
end
With Rails 3.2, error pages have been extracted to a Rack Middleware. Fortunately for us, so
are the application routes. We can to tell rails to use our routes app for the error pages in
config/application.rb
:
# config/application.rb
module YourApp
class Application < Rails::Application
# ...
config.exceptions_app = self.routes
end
end
And that’s it! Add some page templates in app/views/errors/
and you should be good to go.
To test it, we need to turn off the development mode error pages. Set consider_all_requests_local
to false in config/environments/development.rb
:
# config/environments/development.rb
YourApp::Application.configure do
# ...
config.consider_all_requests_local = false
# ...
end
You’ll want to set that back to true before committing your code, or else you won’t receive any helpful feedback while developing your application.
All of the above steps were fairly simple, but figuring out how to determine which format to return
is a little complex. env['REQUEST_PATH']
contains the error path (e.g. “/404”) and no format
information is getting passed to the errors controller, so the standard Rails respond_to
stuff is
not going to work here. We can grab the original request path via env['ORIGINAL_FULLPATH']
. I
added a few private methods to errors controller to help out. The first two help me figure out what
format the request came as. I want to return a JSON response if the request starts with ‘/api’ or
ends in ‘.json’. The third will render our template in the format we want.
class ErrorsController
private
def api_request?
env['ORIGINAL_FULLPATH'] =~ /^\/api/
end
def json_request?
env['ORIGINAL_FULLPATH'] =~ /\.json$/
end
def render_error(error)
if api_request? or json_request?
render "#{error}.json.jbuilder"
else
render "#{error}.html.haml"
end
end
end
Now we just need to update the actions in our controller:
class ErrorsController
def not_found
render_error "not_found"
end
def unprocessable
render_error "unprocessable"
end
def internal_server
render_error "internal_server"
end
end
And we’re done! Make sure you add your json templates to app/views/errors
.
$ curl http://localhost:3000/api/v1/nothing-here
{ error: "not_found" }
$ curl http://localhost:3000/nothing-here.json
{ error: "not_found" }
$ curl http://localhost:3000/nothing-here
<h1>404 Not Found</h1>