Monday, August 4, 2008

Inheriting before_filters and naming collisions

I've been using Ruby on Rails on and off now for over a year for different kinds of projects. It's great for a quick prototype or creating simple web applications, but there is so much "rails magic" that goes on behind the scenes that I doubt anyone really knows how Rails works.

As a programmer, I like to know how every part of my application works, especially when I need to debug a problem when something isn't working right.

When I started working today I updated my local copy to the head revision in the repo, and after I logged into our application, I was redirected to the edit user page (this was expected), but I was shown the error "Couldn't find User without an ID". As I continued to play around, I noticed that logging in worked fine until I went to a page handled by my Users controller.

The only change since the update in the users controller was in the login method:


class UsersController < ApplicationController
def login
session[:user] = nil
end
end
The addition of the single line in it was the only change, but it makes sense; when someone accesses the log in page, the current user should probably be logged out. Since this was the only change, I removed the line, and magically everything seemed to work again.

But why would this cause the session[:user] to be set to nil whenever I hit the Users Controller. The answer to that lies within my Application Controller which users a before_filter :login to make sure you are logged in before you can access anything in the site.

class ApplicationController < ActionController::Base
before_filter :login

def login
if !session[:user]
session[:previous_page] = request.request_uri
redirect_to "/users/login"
end

end
end p.s. Yes, I do irrationally hate the unless method :P


As it turns out, the before_filter is inherited by all of the subclasses of my Application Controller, which is all of them. This allows the before_filter to be called no matter which controller is hit.

However, since I also have a login method in my Users Controller, a naming collision occurred and whenever I accessed the Users Controller, the login method (the one in the Users Controller) would be called. This would clear session[:user], which is what caused the errors.

My first step in solving the problem was to rename the login method in my Application controller

class ApplicationController < ActionController::Base
before_filter :check_user_logged_in

def check_user_logged_in
if !session[:user]
session[:previous_page] = request.request_uri
redirect_to "/users/login"
end

end
end

This should stop the naming collision, but it also caused a new error for me; I would go into an infinite redirect loop when I would go to the log in page. The application would check to see if a user was logged in, which there was not, and redirect to the login page. Before loading the login page, the filter was called again, and the same result happened ad infinitum.

The solution to this problem was to add a skip_before_filter to my Users controller, which would override the before filter in the Application controller so it would not be called.

class UsersController < ApplicationController
skip_before_filter :check_user_logged_in, :only => ["login", "authenticate"]

def login
session[:user] = nil
end

def authenticate
#code for handling user authentication went here
end


Now, when we access the login page, we still clear any logged in user, but the before_filter doesn't check for a logged in User when we go here, as well as when we go through the authenticate method.

This should fix the problems I was having, but with Rails theres always another mysterious bug lurking behind the great unknown.