By default, Rails doesn't handle various errors that normal people make when using web applications. When URLs are being cut and pasted from email clients, they can often lose characters; when people bookmark pages inside an application, the bookmarks may be rendered invalid by someone else making changes to the system (e.g. deleting records). These and other user "errors" can make Rails produce unfortunate error messages, which could baffle normal users, even with the application running in the production environment. For example, here's what happens if you try to display a person whose record doesn't exist (in the production environment):
It turns out there are five common classes of error to catch:
http://localhost:3000/people/foo
, you will get an Unknown action: No action responded to foo error message. This is because Rails can't map the foo
part of the URL onto a valid action in the PeopleController
class. http://localhost:3000/bar
, you will get a Routing Error: no route found to match "/bar" with {:method=>:get} error message. This is because none of the routes specified in the routing configuration can map the URL to a valid controller and action.Each of these errors is relatively easy to catch and handle.
Missing record exceptions (of the class ActiveRecord::RecordNotFound)
occur where a model's find
method is called with an ID that doesn't match a record in the database. One approach to catching this type of error would be to put error catching code directly into PeopleController
. For example, we could modify the get_person
method to redirect to the index if the Person.find
method call raises an exception:
private def get_person @person = Person.find(params[:id]) rescue redirect_to_index 'Person could not be found' end
This utilizes the rescue
construct available in Ruby (similar to the try...catch
or try...except
of other languages) to capture any errors raised by Person.find
. It then reuses the redirect_to_index
method if such an error occurs (see Creating Application-Level Controller Methods in Chapter 5) to set a message and show the index page of the current controller.
The above approach can be useful where you want very fine-grained control over exceptions. However, there are several situations in the Intranet application where this type of exception can occur, and fine-grained exception handling is not really necessary: a more generic approach would be more suitable. Rails provides a controller-level hook called rescue_action_in_public
that we can exploit to manage errors in a more generic fashion. Add the following method definition to app/controllers/application.rb
(inside the class
definition):
protected def rescue_action_in_public(exception) if exception.is_a?(ActiveRecord::RecordNotFound) @message = "Record not found" end end
The rescue_action_in_public
method accepts exceptions thrown by actions, enabling you to change the response depending on the kind of exception raised (is_a? is used here to check the class of the exception). In this case, we are just setting up an instance variable @message
with a user-friendly error message. However, if you navigate to a non-existent record you'll still get a stack trace of the error, even in the production environment. This is because we are working on localhost: Rails knows that we are working on the same machine where the server is running, and thus is assuming we're really developing and not in production.
To fix this assumption, add another method definition to app/controllers/application.rb:
protected def local_request? false end
The local_request?
method is called each time a controller action is triggered. By default, it returns true
if the client IP address is 127.0.0.1; by making it return false
in all situations, no requests are treated as local by virtue of their IP address.
Now, in the production environment, you should see an "Application error" page instead of a stack trace for missing record errors. In the development environment, you will still get a stack trace for these errors: Rails still treats every request as local when the application is running in that environment. This behavior is governed by the production environment settings in config/environments/development.rb
, namely:
config.action_controller.consider_all_requests_local = true
If you want to run your application in the development environment (with automatic reloading of changes to classes and templates), but still test your error catching code, set this value to false
to get the non-local error messages.
Rails also provides a generic rescue_action
method, which works in almost the same way as rescue_action_in _public
(it takes an exception as an argument, and you can respond to different classes of exception inside its body). The only difference is that rescue_action
doesn't care whether requests are local or not: it will always perform the error trapping you define inside it. I'd only recommend using this if you never want to display stack traces in the browser, and just want to work directly with the log files.
To display a custom page with an error message, create a new page in app/views/shared/exception.rhtml:
<h1>An exception occurred</h1> <p class="exception"><%= @message %></p>
Note that it renders the @message
instance variable, set by rescue_action_in_public
, inside a paragraph.
Next, add a new style to public/stylesheets/base.css
to style the error message in the template:
.exception { color: red; }
Then render that template from the rescue_action_in_public
method:
protected
def rescue_action_in_public(exception)
if exception.is_a?(ActiveRecord::RecordNotFound)
@message = "Record not found"
end
render :template => 'shared/exception'
end
Now, when you generate a missing record error by entering a bogus person ID, you should see a styled error page when running in production or when consider_all_requests_local
is set to false:
Rails' default behavior is to render a plain HTML page when errors occur, located in the public directory: 404.html
for routing errors and 500.html
for general errors. While this works OK, it doesn't show the error messages in the context of the application (menus, color schemes, logos, etc.). The advantage of the approach shown here is that the error pages can still use the layouts you've defined for your controllers.
These types of errors occur when Rails attempts to service a URL that maps onto a non-existent action (ActionController::UnknownAction) and/or non-existent controller (ActionController::RoutingError), or if the URL cannot be parsed at all (again, a RoutingError)
. As these cases are effectively equivalent to HTTP "Page not found" errors (with status code 404), a logical approach is to reproduce a "Page not found"-style error inside the application. We can actually just extend the error-catching code inside rescue_action_in_public
to do this:
protected
def rescue_action_in_public(exception)
if exception.is_a?(ActiveRecord::RecordNotFound)
@message = "No record with that ID could not be found"
elseif exception.is_a?(::ActionController::UnknownAction) or
exception.is_a?(::ActionController::RoutingError)
@message = "Page not found"
end
,
render :template => 'shared/exception'
end
The only thing that's unusual about this is how the class of the exception is referenced:::ActionControllerRoutingError, with two colons at the beginning, before the module name. This is to do with Ruby namespaces: because we are sitting inside the ApplicationController
class, we have to work up from this class to the ActionController
module (think of '::' as similar to '..' when defining paths in the href
attribute of an HTML<a>
element).
Occasionally, your application will throw errors that aren't due to user error, but down to bugs or parts of your infrastructure disappearing (e.g. the MySQL server breaking). Try stopping your MySQL server and see what happens to your application: because you've defined rescue_action_in_public
, you will get a Mysql::Error
in the development environment; or your exception template (app/views/shared/exception.rhtml) in the production environment, sans error message.
As we can't be sure of all the sundry error messages we might possibly suffer, we just need to apply a generic else
to our current error catching code to handle all of them:
protected
def rescue_action_in_public(exception)
if exception.is_a?(ActiveRecord::RecordNotFound)
@message = "No record with that ID could not be found"
elseif exception.is_a?(::ActionController::UnknownAction) or
exception.is_a?(::ActionController::RoutingError)
@message = "Page not found"
else
@message = "The application is not currently available"
end
render :template => 'shared/exception'
end
This type of error is a different kettle of fish, and requires a different approach. We're talking drastic errors: the kind where Rails itself implodes, and the application doesn't even raise its head above the parapet. This can happen if FastCGI is running your application and ties itself in knots, for example. The end user gets the dreaded "application error" as a response:
These kinds of errors are unpredictable and hard to produce on demand. The response from the server also varies according to the type of server. For example, if you're running FastCGI under Apache, you will typically be seeing an error returned by Apache: the request never reaches Rails, as Rails itself isn't running properly.
The exact text rendered for this kind of error is defined at the bottom of the file RAILS_ROOT/public/.htaccess:
ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
This file is an Apache control file, which tells Apache what to do if an error occurs inside a Rails application running under CGI or FastCGI. If you want some text more in keeping with your application, you can either manually code some HTML here; or (better) create a custom HTML error page, styled in the same way as your application, and set the ErrorDocument
directive to point at that. There is a template for this in public/500.html
already, so you can edit that as a starting point; then, to set that as the error document, change the ErrorDocument
directive to:
ErrorDocument 500 /500.html
If you are running an application under Mongrel, with Apache sitting in front of it acting as a proxy, the most likely error you'll get will be this one (a 503, rather than a 500 error):
If you see this error, it means your Mongrel process needs restarting. See Chapter 9 for more information about keeping an Apache/Mongrel combination up and running.
3.14.144.108