
There is a lot of information out there about custom error pages in Rails 3.2 and how to use the built-in routing and the exceptions_app setting to get the basics working. Unfortunately, everything I found was missing a piece or two, so here’s my attempt at tying it all together.
Beyond basic 404 and 500 pages
If all you want is to route RecordNotFound and routing errors to 404 and everything else to 500, it’s pretty easy. Any of the posts in the references at the bottom can help you there.
In my case, I also wanted a 401 page for cases where users were attempting to access things they were not authorized to see. I wanted to be able to raise an exception anywhere in the code and have it result in responding to the user with a custom 401 page. 401 Unauthorized falls outside the default exceptions that Rails supports, so I had to create my own exception and tell Rails what to do when it occurred.
First, define the exception:
app/controllers/application_controller.rb
class UnauthorizedException < Exception; end
Next, tell Rails what to do when it sees it.
config/application.rb
config.action_dispatch.rescue_responses.merge!('ApplicationController::UnauthorizedException' => :unauthorized)
The unauthorized symbol used corresponds to the 401 status code defined in Rack. There is a long list of status codes for Rack, and my best guess is that the symbol is the lowercase/underscore derivation of the string representation. In other words, a 402 Payment Required response can be referred to as :payment_required. So, define as many exceptions as you need and add them to the rescue_responses in your application.rb file.
Routing to the ErrorsController and views
Now that your exceptions are defined, you need to route them to your Errors controller and custom pages.
config/application.rb
config.exceptions_app = self.routes
config/routes.rb
match "/401" to: "errors#unauthorized"
match "/404" to: "errors#not_found"
match "/500" to: "errors#error"
app/controllers/errors_controller.rb
class ErrorsController < ApplicationController
def unauthorized
# Will render the app/views/errors/unauthorized.html.haml template
end
def not_found
# Will render the app/views/errors/not_found.html.haml template
end
def error
# Will render the app/views/errors/error.html.haml template
end
protected
# The exception that resulted in this error action being called can be accessed from
# the env. From there you can get a backtrace and/or message or whatever else is stored
# in the exception object.
def the_exception
@e ||= env["action_dispatch.exception"]
end
end
With this setup, several exceptions with result in a 404 (like RecordNotFound or a routing error), our custom exception will result in a 401, and anything else will result in a 500. Our ErrorsController actions (unauthorized, not_found, and error, respectively) will be called and their respective templates rendered. Once you get to this stage, it’s pretty magical. You can render the view and display whatever message you stuffed into the exception, like, “You are not an authorized user for the FooBar group.” or “We can’t find a page named FooBar in our system, are you sure you spelled it right?” It’s very nice to have a generic error page that can also display a specific message related to the request that generated the error.
At this point, you’re done and ready to ship. Unless you like testing, in which case read on…
Testing with capybara
Once it was all finally wired together, I wanted to write some Capybara integration tests to verify that my 404 and 401 pages were displaying correctly when the corresponding exceptions were raised. However, out of the box, Capybara and Rails are set up to error out on the exception instead of actually displaying the error page. With a little digging, I found the settings to change this
config/environments/test.rb
config.consider_all_requests_local = false
config.action_dispatch.show_exceptions = true
The show_exceptions setting is probably a little controversial. On one side, if you set it to false, you can structure your Capybara specs to expect certain exceptions, like so:
lambda {
visit some_protected_path
}.should raise_error(ActionController::UnauthorizedException)
While it looks clean, I really don’t like this. From a user’s perspective, I don’t really care if an exception was successfully raised. What I really want to know is: Did they see my custom 401 page? By setting show_exceptions to true, it will pass all the way through the exception routing and show the 401 page. My specs look like this
visit some_protected_path
page.should have_content "Sorry, you are not authorized to see this page"
page.driver.status_code.should == 401
Ultimately it would be nice to have show_exceptions on to test the full execution of exception handling, then turn it off for other specs to make them a little more readable using the lambda example above. I tried messing with changing the setting in the middle of running the test suite, and the results were pretty miserable. It seems that the setting is read once and further changes after that are ignored.
Is it worth it?
Whenever Rails introduces a new way of doing things, I try to give it a fair shake. In this case, I’m unconvinced that all this work to tie exceptions to custom error pages is any better than just manually routing to the page in case of an error. For example:
app/controllers/application_controller.rb
class ApplicationController
protected
def unauthorized
render status: 401, template: "/errors/unauthorized.html.haml"
false
end
def not_found
render status: 404, template: "/errors/not_found.html.haml"
false
end
With this simple code, you can now route to your custom error anywhere you want in a controller, just like so:
return unauthorized if some_condition_here?
Or, stick it in a before_filter and the false return will halt the filter chain and jump straight to the rendering.
It’s much easier to set up than the exception routing, and in my humble opinion, it works just as well. I’d be curious if anyone can explain why the fancy exception routing is superior. I’m glad to have played with it, but I don’t think I’ll be pushing that hard for it in future apps.
References
- The Pickard Ayune
- coderwall
- Platformatec Blog
- List of Rack known HTTP status codes
- Exceptions Rails handles by default
Special Thanks
I’d like to thank William Metz of Simplephoto.com for his help in navigating the Rails exception routing labyrinth!