Looking at the page displayed in Figure8.2, it’s apparent that our application threw an exception at line 16 of the store controller.5That turns out to be the line
product = Product.find(params[:id])
If the product cannot be found, Active Record throws aRecordNotFoundexcep- tion,6 which we clearly need to handle. The question arises—how?
We could just silently ignore it. From a security standpoint, this is probably the best move, because it gives no information to a potential attacker. How- ever, it also means that should we ever have a bug in our code that gener- 5. Your line number might be different. We have some book-related formatting stuff in our source files.
6. This is the error thrown when running with MySQL. Other databases might cause a different error to be raised. If you use PostgreSQL, for example, it will refuse to acceptwibbleas a valid value for the primary key column and raise aStatementInvalidexception instead. You’ll need to adjust your error handling accordingly.
Figure 8.2: Our Application Spills Its Guts
ates bad product ids, our application will appear to the outside world to be unresponsive—no one will know there has been an error.
Instead, we’ll take three actions when an exception is thrown. First, we’ll log the fact to an internal log file using Rails’ logger facility (described on page243). Second, we’ll output a short message to the user (something along the lines of “Invalid product”). And third, we’ll redisplay the catalog page so they can continue to use our site.
The Flash!
As you may have guessed, Rails has a convenient way of dealing with errors and error reporting. It defines a structure called a flash. A flash is a bucket (actually closer to aHash) in which you can store stuff as you process a request. The contents of the flash are available to the next request in this session before being deleted automatically. Typically the flash is used to collect error messages. For example, when ouradd_to_cartaction detects that it was passed an invalid product id, it can store that error message in the flash area and redirect to the index action to redisplay the catalog. The view for the index action can extract the error and display it at the top of the catalog page. The flash information is accessible within the views by using the flash accessor method.
Why couldn’t we just store the error in any old instance variable? Remember that after a redirect is sent by our application to the browser, the browser sends a new request back to our application. By the time we receive that
request, our application has moved on—all the instance variables from previ- ous requests are long gone. The flash data is stored in the session in order to make it available between requests.
Armed with all this background about flash data, we can now change our add_to_cartmethod to intercept bad product ids and report on the problem.
Download depot_h/app/controllers/store_controller.rb
def add_to_cart begin
product = Product.find(params[:id]) rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}" ) flash[:notice] = "Invalid product"
redirect_to :action => :index else
@cart = find_cart
@cart.add_product(product) end
end
Therescueclause intercepts the exception thrown by Product.find. In the han- dler we
• Use the Rails logger to record the error. Every controller has a logger attribute. Here we use it to record a message at theerrorlogging level. • Create a flash notice with an explanation. Just as with sessions, you
access the flash as if it were a hash. Here we used the key:noticeto store our message.
• Redirect to the catalog display using theredirect_tomethod. This takes a wide range of parameters (similar to the link_to method we encountered in the templates). In this case, it instructs the browser to immediately request the URL that will invoke the current controller’s index action. Why redirect, rather than just display the catalog here? If we redirect, the user’s browser will end up displaying a URL ofhttp://.../store/index, rather than http://.../store/add_to_cart/wibble. We expose less of the application this way. We also prevent the user from retriggering the error by hitting the Reload button.
This code uses a little-known feature of Ruby’s exception handling. The else clause invokes the code that follows only if no exception is thrown. It allows us to specify one path through the action if the exception is thrown and another if it isn’t.
With this code in place, we can rerun our customer’s problematic query. This time, when we enter the URL
we don’t see a bunch of errors in the browser. Instead, the catalog page is dis- played. If we look at the end of the log file (development.login thelogdirectory), we’ll see our message.7
Parameters: {"action"=>"add_to_cart", "id"=>"wibble", "controller"=>"store"}
Product Load (0.000427) SELECT * FROM products WHERE (products.id = 'wibble') LIMIT 1 Attempt to access invalid product wibble
Redirected to http://localhost:3000/store/index Completed in 0.00522 (191 reqs/sec) . . . Processing StoreController#index ...
: :
Rendering within layouts/store Rendering store/index
So, the logging worked. But the flash message didn’t appear on the user’s browser. That’s because we didn’t display it. We’ll need to add something to the layout to tell it to display flash messages if they exist. The following rhtml code checks for a notice-level flash message and creates a new<div>containing it
if necessary.
<% if flash[:notice] -%>
<div id="notice" ><%= flash[:notice] %></div> <% end -%>
So, where do we put this code? Wecouldput it at the top of the catalog display template—the code inindex.rhtml. After all, that’s where we’d like it to appear right now. But as we continue to develop the application, it would be nice if all pages had a standardized way of displaying errors. We’re already using a Rails layout to give all the store pages a consistent look, so let’s add the flash- handling code into that layout. That way if our customer suddenly decides that errors would look better in the sidebar, we can make just one change and all our store pages will be updated. So, our new store layout code now looks as follows.
Download depot_h/app/views/layouts/store.rhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" > <html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot" , :media => "all" %> </head>
7. On Unix machines, we’d probably use a command such astailorlessto view this file. On Win- dows, you could use your favorite editor. It’s often a good idea to keep a window open showing new lines as they are added to this file. In Unix you’d usetail -f. You can download atailcommand for Windows fromhttp://gnuwin32.sourceforge.net/packages/coreutils.htmor get a GUI-based tool from http://tailforwin32.sourceforge.net/. Finally, some OS X users findConsole.app(inApplications→Util- ities) a convenient way to track log files. Use theopen command, passing it the name of the log file.
<body id="store" > <div id="banner" >
<img src="/images/logo.png" />
<%= @page_title || "Pragmatic Bookshelf" %> </div>
<div id="columns" > <div id="side" >
<a href="http://www...." >Home</a><br />
<a href="http://www..../faq" >Questions</a><br /> <a href="http://www..../news" >News</a><br /> <a href="http://www..../contact" >Contact</a><br /> </div>
<div id="main" >
<% if flash[:notice] -%>
<div id="notice" ><%= flash[:notice] %></div> <% end -%> <%= yield :layout %> </div> </div> </body> </html>
We’ll also need a new CSS styling for the notice box. Download depot_h/public/stylesheets/depot.css
#notice {
border: 2px solid red; padding: 1em;
margin-bottom: 2em; background-color: #f0f0f0 ; font: bold smaller sans-serif; }
This time, when we manually enter the invalid product code, we see the error reported at the top of the catalog page.
Sensing the end of an iteration, we call our customer over and show her that the error is now properly handled. She’s delighted and continues to play with
David Says. . .
How Much Inline Error Handling Is Needed?
The add_to_cart method shows the deluxe version of error handling in Rails where the particular error is given exclusive attention and code. Not every conceivable error is worth spending that much time catching. Lots of input errors that will cause the application to raise an exception occur so rarely that we’d rather just treat them to a uniform catchall error page.
We talk about setting up a global error handler on page627.
the application. She notices a minor problem on our new cart display—there’s no way to empty items out of a cart. This minor change will be our next itera- tion. We should make it before heading home.