#53 Handling Exceptions (revised)
- Download:
- source codeProject Files in Zip (61.5 KB)
- mp4Full Size H.264 Video (24.6 MB)
- m4vSmaller H.264 Video (12.2 MB)
- webmFull Size VP8 Video (15.1 MB)
- ogvFull Size Theora Video (27.5 MB)
Exceptions are bound to happen in a Rails application, either because of an error in our application’s code or on the user’s side. Rails treats both of these in a similar fashion: in development it shows the familiar debug page to help use to resolve what we’ve done wrong while in production it renders a static error page. We can simulate the production environment while in development by modifying the development config file and changing one of the settings.
config.consider_all_requests_local = false
Alternatively we can set up a staging environment to do this, like we showed in episode 72. After we restart our Rails application and trigger an exception we’ll see the static error message page as if our application was in production.
By the way, before we deploy an application we should update the static error pages to better fit its design as this can be easily forgotten. We’ll find these pages under the /public
directory. Three different status codes are handled by default but we can easily add more to handle other cases. Let’s say that we want to handle 403 Forbidden
errors we can create a 403.html
page.
<!DOCTYPE html> <html> <head> <title>Forbidden (403)</title> <style type="text/css"> body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } div.dialog { width: 25em; padding: 0 4em; margin: 4em auto 0 auto; border: 1px solid #ccc; border-right-color: #999; border-bottom-color: #999; } h1 { font-size: 100%; color: #f00; line-height: 1.5em; } </style> </head> <body> <!-- This file lives in public/403.html --> <div class="dialog"> <h1>Forbidden.</h1> <p>You are not authorized to perform that action.</p> </div> </body> </html>
Rails will render this HTML whenever it encounters an exception that is linked to the 403 status. Let’s say that we have a specific type of exception that we want to display this error for. In our ProductsController
we have an custom Forbidden
class defined which inherits from StandardError
and this is raised whenever someone visits the show action. Visiting this page currently triggers a 500
error as Rails doesn’t know how to associate this type of error with the 403
status. To change this behaviour we need to modify the application’s config file.
config.action_dispatch.rescue_responses["ProductsController::Forbidden"] = :forbidden
Here we set config.action_dispatch.rescue_responses
which is a hash where the key represents the name of the exception that we want to handle and the value is the status code, in this case :forbidden
(we can use the status codes that Rack uses instead of a numeric HTTP status). To learn more about how Rack handles status codes take a look at the documentation for Rack::Utils
where we’ll find the names for each status code and how it’s converted to a symbol. If we visit the a product’s page now after restarting our app we’ll see a 403.html
error page.
Next we’ll show you some of the exceptions that Rails maps by default. For example if we trigger a route that doesn’t exist we’ll see a 404
error instead of a 500
. Most of these are defined in the Rails source code in the ExceptionWrapper
class. This class sets the default rescue_responses
hash that we configured earlier and one of the values set is ActionController::RoutingError
which is set to :not_found
. This is what we see in the application with the 404
status.
Dynamic Error Pages
This is all well and good but what if we want something more dynamic in our error pages. We’re currently displaying static content but what if we want to change the message to something more specific to the user’s situation. One way to do this is through the controller. We can call the rescue_from
method to override the behaviour of certain exceptions.
class ApplicationController < ActionController::Base protect_from_forgery rescue_from "ProductsController::Forbidden", with: :forbidden private def forbidden(exception) render text: exception.message end end
Here we call a forbidden
method when our ProductsController::Forbidden
exception is raised. To keep things simple we just render the text of the exception that’s raised so that we can see the dynamic behaviour. When we reload the page that raises that exception now we’ll see our custom error.
We could instead render a template here or perform a redirect with a flash message and this is a good use case for using rescue_from
when we have a custom defined exception that we’re handling. If we want to turn any kind of exception into a dynamic error page through this, however, rescue_from
has limitations. To demonstrate this we’ll try handling every kind of exception this way.
class ApplicationController < ActionController::Base protect_from_forgery rescue_from "Exception", with: :forbidden private def forbidden(exception) render text: exception.message end end
When we reload the page it works like it did before and if we try to visit the page for a product that doesn’t exist we’ll see the text for a RecordNotFound
exception. If we visit a page which doesn’t have a route an exception is raised and this isn’t handled by the controller as it’s raised outside the controller layer. There are several other reasons why we might not want to use rescue_from
: it completely overrides the exception behaviours so we need to check whether we’re in development or production and if we try to map these back to HTTP status codes it’s difficult to do from here as it’s so early on. Also this doesn’t trigger any kind of exception notification emails or alerts we may have set up. In general if we’re trying to make error pages dynamic this isn’t the best approach.
Handling Exceptions With Middleware
Instead it’s better to handle this through Rack middleware. Among the middleware for our application is one called ActionDispatch::ShowExceptions
which handles rescuing from exceptions and rendering out the error. If we look at the source code for this we’ll see that its initializer accepts something called exceptions_app
which is a Rack app that the exception handling is delegated to and by default it delegates to something called PublicExceptions
which is a Rack app built into Rails and which handles rendering the static HTML error pages. If we want to do something dynamic here instead we can define our own Rack app and supply this as the exceptions app instead. To do this we just have to modify our application’s configuration file and set the exceptions_app
configuration option. Instead of defining a custom Rack app to handle this we can use our Rails app itself. We can do this by defining self.routes
as the exceptions_app
.
config.exceptions_app = self.routes
To get this work to work we need to configure our routes to handle the different error status codes. The code will be passed in as a path so we can match them to anything we want. For example we could redirect any 404 errors to the home page.
Store::Application.routes.draw do resources :products root to: 'products#index' match '404', to: redirect('/') end
Now when we try to visit a page which doesn’t exist we’re redirected back to the home page. This is really powerful as instead of redirecting we can trigger any controller action. We’ll generate a new controller for this that we’ll call Errors
.
$ rails g controller errors
We could use this to redirect 404
errors to a not_found
action but instead we’ll make the route more generic and have any status code route to the controller’s show action. We’ll use constraints to ensure that only paths that are made up of three digits are matched. Note that in Rails 4 this call to match
will cause a problem and we’ll need to add a :via
option and set it to :all
.
match ':status', to: 'errors#show', constraints: {status: /\d{3}/ }
We can now define the ErrorsController
’s show
action. For now we’ll just display the HTTP status and while we might expect to be able to do this by rendering params[:status]
the params actually carry over from the controller action that caused the error so instead we need to use the request.path
.
class ErrorsController < ApplicationController def show render text: request.path end end
Now when an exception is raised is is handled through the ErrorsController
and the status is displayed.
We can now render out a dynamic template for any given error. If we create four view templates under /app/views/errors
we can use these to display a message relevant to the status.
<h1>Forbidden.</h1> <% if @exception %> <p><%= @exception.message %></p> <% else %> <p>You are not authorized to perform that action.</p> <% end %>
We’ll call these from the controller, stripping off the initial slash from the path so that the template based on the status number is called.
def show render action: request.path[1..-1] end
When we visit a non-existent page now we’ll see the 404
template.
We can make pages more dynamic based on the exception that was raised. We can get the exception from an environment variable which is set by the middleware.
def show @exception = env["action_dispatch.exception"] render action: request.path[1..-1] end
We can then use this to display the exception’s message, if it exists.
<h1>Forbidden.</h1> <% if @exception %> <p><%= @exception.message %></p> <% else %> <p>You are not authorized to perform that action.</p> <% end %>
Now when an exception is triggered the specific error message is shown.
Another thing we can do in the ErrorsController
is support different formats. For example if we receive a JSON request we should present the error in a format that can be accepted. We can do that like this:
def show @exception = env["action_dispatch.exception"] respond_to do |format| format.html { render action: request.path[1..-1] } format.json { render json: {status: request.path[1..-1], error: @exception.message}} end end
Here we use respond_to and respond to HTML requests like we did before and respond to JSON requests with a hash containing the status and exception message.
This is great. We can now handle exception error messages dynamically and this gives us a lot of options. That said it’s best not to go overboard here and especially to make sure that we don’t cause an exception to be raised in the page that handles what to do when an exception is raised.
Even if we do have dynamic pages like this it’s a good idea to keep the static versions in the /public
directory up to date and fitting our application’s design in case these are used by the web server itself. We can do this by generating these from our dynamic pages. If we do this we should modify the route that handles the error pages so that it supports an optional errors
path as otherwise it will try to use the public page.
match '(errors)/:status', to: 'errors#show', constraints: {status: /\d{3}/}
With this in place we can regenerate the static error pages at any time by making a request to the dynamic error page and passing the output to a static error page.
$ curl localhost:3000/errors/404 > public/404.html
We can do this whenever we need to or set up a script to do it automatically.