Skip to main content

Rails: Dynamic 404s, authlogic, Cucumber, and rescue_from

I'm using Rails, Cucumber, and authlogic. I want my 404 pages to be rendered dynamically; that way, I can use my application-wide layout, which is a lot friendlier for lost users. I want ActiveRecord::RecordNotFound exceptions to be handled by the dynamic 404 page. I want Cucumber tests to verify that everything is working correctly.

Getting everything working together at the same time turned out to be extremely challenging. Cucumber made it hard for me to test my ActiveRecord::RecordNotFound handling. authlogic made it impossible for me to catch ActionController::RoutingError exceptions. Defining ApplicationController#render_optional_error_file as described here conflicted with authlogic and/or Cucumber. After several hours, I finally got it all working. Here's how:

First, I added the following to ApplicationController:
# Note, I can't use rescue_from to catch ActionController::RoutingError,
# otherwise authlogic breaks. Hence, I set a default route instead.
rescue_from ActiveRecord::RecordNotFound, :with => :render_404
...
# I'm handling 404s manually so that I can use the application-wide layout.
def render_404
render :template => "errors/error_404", :status => "404 Not Found"
end
Note, render_404 must be a public method.

At the bottom of routes.rb, I have:
# If all else fails, render a 404.
map.connect '*path', :controller => :application, :action => :render_404
In app/views/errors/error_404.html.erb, I have:
Sorry, the page you were looking for does not exist.
That takes care of rendering 404s if no routes match or if there is an ActiveRecord::RecordNotFound exception.

My Cucumber tests look something like:
Scenario: there should be a custom 404 page for routing errors
Given I am on the homepage
And I am simulating a remote request
When I am on an invalid URL
Then I should get a "404 Not Found" response
And I should see "Sorry, the page you were looking for does not exist."
And I should see "Log In"

Scenario: there should be a custom 404 page for record not found errors
Given I am logged in as admin
And I am simulating a remote request
When I am on an invalid user
Then I should get a "404 Not Found" response
And I should see "Sorry, the page you were looking for does not exist.
In features/step_definitions/helper_steps.rb, I have:
def assert_response_status(http_status, message)
response.status.should == "#{http_status} #{message}"
end

Then /^I should get a "(\d+) ([^"]+)" response$/ do |http_status, message|
assert_response_status(http_status, message)
end

Given /^I am simulating a remote request$/ do
header "REMOTE-ADDR", "10.0.1.1"
end
Note, the REMOTE-ADDR header is necessary or else Rails will render a normal exception page when you're trying to test the 404 page.

In features/support/env.rb b/features/support/env.rb I have:
# Comment out the next line if you want Rails' own error handling
# (e.g. rescue_action_in_public / rescue_responses / rescue_from)
#
# I'm commenting it out in order to test that my 404 handling (using
# rescue_from) works properly.
# Cucumber::Rails.bypass_rescue
Update: Please see the comments on bypass_rescue and @allow-rescue below.

Figuring that out took the longest. Cucumber was monkey patching Rails behind my back. I spent a few hours trying to figure out why my Rails rescue_from clause wasn't running when it turned out that Cucumber was to blame.

In features/support/paths.rb, I have these paths defined:
when /an invalid URL/
"/totally_bogus_dude"
when /an invalid user/
user_path(-1)
Finally, I added the following to public/404.html:
<!--
In theory, this file should no longer be necessary since I'm handling 404s
dynamically. I'm going to leave it here in case something breaks behind my
back.
-->
Whew! That was rough!

Comments

Dom said…
Thanks for you hard work ;-) and sharing it. Very much appreciated.
Thanks! That makes it all worthwhile ;)
HansCz said…
Second that. Thanks for the writeup. I appreciate it.

One thing, though. Am I correct in assuming that neither of your scenarios cover the case of raising ActiveRecord::RecordNotFound without being logged in?

So either it ought not be possible to ask for records without being logged in, or Rails has a default behaviour for the case. If it does, what would that be in a production environment?

I am a bit at a loss for finding a good overview of the Rails error handling. Maybe you have some references?

Thanks in advance Shannon
> Second that. Thanks for the writeup. I appreciate it.

My pleasure.

> One thing, though. Am I correct in assuming that neither of your scenarios cover the case of raising ActiveRecord::RecordNotFound without being logged in?

I just logged out. Then, I purposely raised an ActiveRecord::RecordNotFound exception in my controller. It lead to a 404.

> So either it ought not be possible to ask for records without being logged in, or Rails has a default behaviour for the case. If it does, what would that be in a production environment?

Sorry, I don't understand what you're getting at. Access control checks are done before the controller action gets executed. When the controller action gets executed, it might raise an ActiveRecord::RecordNotFound exception, and that'll lead to a 404.

You wrote "So either it ought not be possible to ask for records without being logged in, or Rails has a default behaviour for the case." However, Rails doesn't even have a concept of "being logged in", that comes from authlogic.

> I am a bit at a loss for finding a good overview of the Rails error handling. Maybe you have some references?

I think this is what you're looking for: http://ryandaigle.com/articles/2007/9/24/what-s-new-in-edge-rails-better-exception-handling

Happy Hacking!
HansCz said…
> orry, I don't understand what you're getting at. Access control checks are done before the controller action gets executed. When the controller action gets executed, it might raise an ActiveRecord::RecordNotFound exception, and that'll lead to a 404.

The case I meant was just this: Given the site has your code included and it is also is set up so I can access some pages that do ActiveRecord queries without being logged in.
What happens then if I get an ActiveRecord::RecordNotFound?

By your explanation it sounds like I'll get a 404 when raising ActiveRecord::RecordNotFound from said pages.

Thanks for the link
> By your explanation it sounds like I'll get a 404 when raising ActiveRecord::RecordNotFound from said pages.

Yep, whether you're logged in or not, anytime an ActiveRecord::RecordNotFound isn't caught and would otherwise lead to a server error, a 404 is returned instead.
HansCz said…
Sounds like acceptable behaviour to me. Thanks for the help, Shannon.

I admire your persistence in solving the problems you encounter. Keep those posts coming. I'm sure you're saving people a lot of time.
Thanks for the encouragement! ;)
Cucumber supports an @allow-rescue tag which lets you tag specific scenarios that should be have rescues allowed. This allows you to only allow rescues for scenarios where you actually need rescues to be allowed.
> Cucumber supports an @allow-rescue tag

Yes, thanks for bringing that up. I'm using that in my own tests.
Henrik said…
To clarify: bypass_rescue is no longer supported at all, and you should instead use the @allow-rescue tag as someone mentioned.
Yep, thanks for the update. I updated my code, but not the blog post.
Grant Neufeld said…
Thank-you for ending my suffering! I spent a few hours tracing through the cucumber and rails code to figure out why my rescue_from calls were working in development, but not in cucumber. @allow-rescue totally saved the day.

I encourage you to put a note in the blog post that this is the new way to deal with the problem. Thanks, again.
Thanks for your comment. I added a note to the blog post.
tjeden said…
Thanks, It saved me a lot of time. :)
Glad to hear it ;)
Anonymous said…
Thanks a lot!! First post which worked immediatly.
AtomicMonster said…
I am doing something similar to this, but I can't get my cucumber test to pass when I am using a javascript driver. Here's what's different in my case: I have a setting for whether to show the custom page. If the render_404 gets called and this setting is true, then it does what yours does; otherwise, it calls rescue_action_without_handler and passes on the exception.

It works great, but when I test with a javascript driver, I can see the template is getting rendered (based on the cucumber.log), but cucumber is getting an empty html file (just html, head, body, and "No File" inside of an h1 in the body).

Any ideas?
Sorry, man, I have no clue. Is it possible that Rails is trying to serve some static 404 page? Is it possible this is simply a timing issue (Selenium is fickle)? Can you put some "puts" statements in the template to verify it's being run correctly? Does it work correctly with a normal browser? Are you outputting something in the template and using <% instead of <%=? Are you sure your call to render is correct?
Anonymous said…
Gj. Thx.
Shaun said…
Hi. Thanks for help in hand.

I just wanted to point out a little thing that had me going round in circles for a few minutes.

In my app, specifying :application in the routes as a symbol caused the following error:

undefined method 'camelize' for :application:Symbol

After looking everywhere else, I realised what was going on and changed the route you specified to:

map.connect '*path', :controller => 'application', :action => 'render_404'

i.e., making the controller a string rather than a symbol.

Hope that helps somebody else out!

Thanks again.

Shaun
Thanks, Shaun. Kind of seems like a bug. They should probably accept either symbols or strings.