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:
At the bottom of routes.rb, I have:
My Cucumber tests look something like:
In features/support/env.rb b/features/support/env.rb I have:
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:
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,Note, render_404 must be a public method.
# 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
At the bottom of routes.rb, I have:
# If all else fails, render a 404.In app/views/errors/error_404.html.erb, I have:
map.connect '*path', :controller => :application, :action => :render_404
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 errorsIn features/step_definitions/helper_steps.rb, I have:
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.
def assert_response_status(http_status, message)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.
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
In features/support/env.rb b/features/support/env.rb I have:
# Comment out the next line if you want Rails' own error handlingUpdate: Please see the comments on bypass_rescue and @allow-rescue below.
# (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
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/Finally, I added the following to public/404.html:
"/totally_bogus_dude"
when /an invalid user/
user_path(-1)
<!--Whew! That was rough!
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.
-->
Comments
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
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!
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
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.
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.
Yes, thanks for bringing that up. I'm using that in my own tests.
I encourage you to put a note in the blog post that this is the new way to deal with the problem. Thanks, again.
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?
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