Handling Errors

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):

Handling Errors

It turns out there are five common classes of error to catch:

  1. Missing records,, as in the example above.
  2. Unknown actions. For example, if you visit 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.
  3. Routing errors. These occur if Rails can't map the URL onto a valid controller. For example, visiting 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.
  4. General application errors. These can occur for any number of reasons: missing Ruby libraries, broken code, infinite loops which overflow the stack, etc. However, in most cases, Rails will still be running and capable of rendering an intelligible error report.
  5. Rails falling over. Occasionally, Rails itself will break and your application will become completely unavailable. This tends to happen if Rails is running on FastCGI (which is still often the case if you run applications on shared hosting), and far, far less when Rails is running on Mongrel. As these errors break Rails itself, they are caught by the web server running the application.

Each of these errors is relatively easy to catch and handle.

Catching Missing Record Errors

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.

Note

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:

Catching Missing Record Errors

Note

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.

Catching UnknownAction and Routing Errors

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).

Catching General Application-Level Errors

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

Catching "Rails has Fallen Over" Errors

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:

Note

Application Error-

Rails application failed to start properly

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):

Note

Service Temporarily Unavailable

The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.144.108