Use whiny_finder and RecordNotFound to turn 500 into 404

Plugins, Ruby on Rails Add comments

What happens when you can’t find the record the user is looking for? Let’s say the user is looking for blog post 9999, and you’ve only got 25 posts. What should happen? Not being an expert in HTTP, I’m not really sure, but returning a 404 in this case seems like a good idea. By default, Rails is set up to translate an unhandled ActiveRecord::RecordNotFound exception into a 404 response to the client (see update below). This is just another one of the nice little hand-holding defaults that makes Rails so friendly to noobs like myself.

To support this, the default find() method is designed to throw a RecordNotFound exception any time you request a record ID that does not exist. So, find(9999) in our example will throw the RecordNotFound exception, and assuming you’re not handling it explicitly, Rails will return a 404 to the client browser. In addition, the 404.html file in your public/web directory will be served out to the client. You can put all sorts of clever hints in here to guide the user to the right spot.

Unfortunately, not all find() methods are created equal. Only the most basic find actually throws the error. This is the find where you pass in the exact ID (or a list of IDs). With other incarnations of the find method, nil is returned but no RecordNotFound is thrown. If you’re using find_by_firstname_and_lastname all over the place, you’ll never get any exceptions. If you’re good, then you’re checking to make sure the return is not nil. If you’re sloppy (like me) you don’t do any checking and tell yourself you’re too busy.

The end result is that you get a nil back and then pass it to your views. At some point, you try to access a property and you get an “undefined method XXXX for nil:NilClass” exception. Unlike the RecordNotFound, however, Rails will return a 500 for this exception. A 500 indicates an internal server error, rather than a page not found. In other words, you’re admitting to the client, “I write sloppy code and something went wrong.”

One solution to the problem is to check every time you call find. If nil is returned, then raise a RecordNotFound exception. This will translate to a 404 and everyone is happy. “But I’m lazy,” you complain. “Bounds checking is just so boring.” Lucky for you (and me) someone else is just as lazy, but a lot more clever. They had the same idea and decided to code this behavior as a plugin, thus allowing the rest of us to DRY out our controllers.

The whiny_finder plugin is a simple and elegant bit of code that adds a “find!” (bang) complement to all your model classes. Following the standard Ruby convention, using the bang version of the method behaves exactly as the original, except when nothing is found a RecordNotFound exception is thrown. Where before we only had find_by_firstname_and_lastname, we now also have find_by_firstname_and_lastname! That last exclamation point makes all the difference.

To get the whiny_finder plugin, just run the standard plugin installer:

At that point, just take a couple minutes and do a search for “find_by_” and “find(:first” In a jiffy you can replace all your regular silent finders with their whiny counterparts. Then you’re set!

By installing a single plugin and spending 10 minutes updating your finders, you can turn tons of 500 “server error” into 404 “page not found” Your users (and friendly robots) will be better informed as to what’s going on, and your sloppy programming skills will once again be safely hidden away. What do you have to lose?

Update: Thanks to Tony in the comments, I have been informed that Rails 1.2.3 is not set up to respond with 404 on an unhandled RecordNotFound exception. This seems to have been added in Edge Rails, however.

It was working for me because I’m using the most up-to-date version of the exception_notification plugin, which seems to have been updated to match Edge Rails. I’m still using 1.2.3 for Rails, but getting exception_notification gave me the updated exception handling.

So, to get the 404s from unhandled RecordNotFound exceptions, you need to do one of the following:

  • Get Edge Rails
  • Get exception_notification (** what I would recommend **)
  • Override rescue_action_in_public in your application.rb controller.

11 Responses to “Use whiny_finder and RecordNotFound to turn 500 into 404”

  1. jerry richardson Says:

    How about handling the RecordNotFound error, setting the flash[:notice] text to “Record not found” and redirecting them to the index page?

  2. Micah Says:

    @Jerry

    It’s totally up to you. You can catch the RecordNotFound if you want to do any special handling. However, I think a 404 in these cases is perfectly acceptable. After all, the user is asking for something that doesn’t exist, and therefore cannot be found.

    Also, by returning the 404, you will let robots know that the page does not exist. If you redirect to the index page, the robots will think it’s a valid redirect, which it definitely is not.

    Don’t forget that your 404 page doesn’t have to be a dry, boring place. You can spice it up all you want. Take a look at our page for Obsidian Portal:
    http://www.obsidianportal.com/not-a-real-page. It could be sexier, but at least it has the same theme as the main site.

  3. Tony Says:

    When you call Model.find(id), you are passing it an exact id, that you presumably have gotten somewhere else, internally. Such as from another record. So when an exact record of a given id is expected, but not found - it’s an exceptional situation and that calls for a RecordNotFound exception to be thrown.

    If you are handling user input, you can’t be sure that valid information is supplied, so you search for it.

    Model.find(:first, :conditions => … )

    You either get the first record that matches, or nil. Which is alright, because you check for such kind of stuff, right? ;)

    Masking your 500s with 404s is like saying “I write _really_ sloppy code. And actively ignore it!”

  4. Micah Says:

    @Tony,

    Assuming you’re checking for nil on user input, what happens when you search for something and get nil? What should you do then? You could redirect them to a page warning them that it wasn’t found. But wait, isn’t that what a 404 page is for?

    Or, you can use whiny_finder to do the checking for you and throw the RecordNotFound exception, which automatically takes them to the 404 page. The end result is pretty much the same.

    I don’t recommend masking 500s as 404s, but the case being dealt with here _is_ a 404. It only becomes a 500 if you don’t (or forget to) do any checking on the value returned from find() and then try to execute an operation on it.

  5. Tony Says:

    Yes, if the user request is not found, you should check for nil and return 404. Fairly simple one liner.

    render :file => “public/404.html”, :status => 404 and return if @result.nil?

    The case here is _not_ a 404 — it’s an exception due to corruption in data. How’d you manage to pass a non-existent id to find in the first place?

    Robots don’t enter obscure ids, just follow your internal links. Users do enter arbitrary data… but then it’s a search, and should be treated as such.

    Anyways, raising an exception on a user search that resulted in no matches will hide all the real 500s that you probably should be fixing.

  6. Micah Says:

    @Tony,

    The following link is an example from Obsidian Portal:
    http://www.obsidianportal.com/campaign/kensing

    “kensing” is the slug for a campaign, and it’s not technically the ID. Still, for all intents and purposes, it is an ID because it’s guaranteed unique.

    So, in the controller for this we have:
    Campaign.find_by_slug(params[:slug])

    Anyways, if a user writes this down and hands it off to a friend as:
    http://www.obsidianportal.com/campaign/kesning (note the typo in the slug) then the URL they created effectively requests a non-existent id. It’s not a search, but simply a request for something that’s not there. If they post this URL on the web somewhere then both humans and bots will find it and try to follow it. I wish I were smart enough to write something that recognizes the typo and takes them to the right place, but for now I’ll settle for the 404.

    By using whiny_finder, I can just use find_by_slug! (bang) and it will automatically result in a 404 if the slug is not found. The one-liner you wrote will work as well, but it has to be done every time you do a similar find. whiny_finder effectively collapses that particular one-liner into the find() method itself. For places where it’s inappropriate, just use the regular, non-bang find().

  7. Tony Says:

    I’d argue that it’s a “search”, although I do agree that 404 is appropriate here (the search was not found).

    My stance is that you raise a 500 exception and reroute to 404.

    If you’re rewriting all find_by_ to find_by_! (bang) anyways, why not skip the plugin and follow Rails Doc’s on the proper usage of find vs find(:first) ? Sure, it’s a few more characters.. but you’d be using proper Rails, *and* actually logging all your real 500s.

  8. Micah Says:

    @Tony,

    Can you please explain what you mean by “raise a 500 exception and reroute to 404″? I’m not clear on what a 500 exception is. I know what a 500 response code is, but the whole point of the article is to convert 500s to 404s (where appropriate…)

    Assuming you mean “raise an exception that would generate a 500″, then I would have to disagree, as it seems overly complicated. In most cases you would raise the exception, then immediately catch it, then reroute to a 404. If you’re vehemently opposed to whiny_finder and RecordNotFound, then just use the one-liner you posted earlier.

    Also, can you please post a link to the Rails Docs you’re referencing regarding the proper usage for find? I’d like to read this. The only stuff I’ve read on find() is from the RDoc for ActiveRecord::Base (located here: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M000992 ), and I can’t find anything specific about when to use find() vs find(:first) vs find_by_().

  9. Tony Says:

    ops, I should have pointed out that, from your first paragraph, “By default, Rails is set up to translate an unhandled ActiveRecord::RecordNotFound exception into a 404 response to the client” is wrong.

    ActiveRecord::RecordNotFound will return a 500 response to the client. The same exception and response that whiny_finder returns.

    I actually take back my arguments against whiny_finder (I got a chance to read through it’s code) — you never get to 404s. In fact, it’s used to raise more 500s on exceptional “searches” ;)

  10. Micah Says:

    @Tony,

    Hmm, this is strange. I was 100% positive that the standard behavior of Rails in the production environment was to return a 404 on a RecordNotFound. I was under the impression that was the whole idea behind whiny_finder (which I didn’t write).

    This backs me up:
    http://shifteleven.com/articles/2006/12/03/trusting-in-exceptions-to-do-what-they-do

    However, when I looked at the source for rescue_action_in_public here: http://api.rubyonrails.org/classes/ActionController/Rescue.html there is no mention of RecordNotFound. It handles RoutingError and UnknownAction exceptions, but no mention of RecordNotFound.

    I guess the behavior I am seeing is a mixture of whiny_finder and exception_notification (available here: http://dev.rubyonrails.org/svn/rails/plugins/exception_notification/ ). The exception_notification plugin (heavily recommended) adds RecordNotFound as an exception that generates a 404.

    Looking at Edge Rails trunk ( http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/rescue.rb at rev 7026) it seems as though Edge Rails has RecordNotFound as an automatic 404, although I didn’t read too deeply. I’m guessing that’s why exception_notification was updated to have the same behavior.

    I will update the article to mention exception_notification, Edge Rails, and/or how to update rescue_action_in_public to translate RecordNotFound into a 404. Doing any of those should result the desired behavior.

    I guess it take 15 minutes instead of 10 ;)

    Thanks for the catch, Tony.

  11. dazza Says:

    here is the easiest solution I have found for catching RecordNotFound using rescue_from

    http://almosteffortless.com/2007/10/08/graceful-404s-in-rails-20/

Leave a Reply

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in