Pothibo

Gems are not a black box: How I debug my own bugs

Sometimes a bug seems very hard to understand. Here's what I do when I see no way out.

Bundle install --path=.bundle

Most of my projects use the normal path when installing bundled gems. That's how it's supposed to be. However, when you have a bug and you don't really know what's happening, it's time to dig into the scary black box that is your gems.

To get started, I want to use a local copy of all the gems because I don't want to screw up a gem in the shared path and forget about it. The goal is not to create more mayhem, you know.

Installing your gems in project_dir/.bundle allow you to screw everything up without affecting any other project that would normally use the same gem i.e. rails.

Open a terminal, and get to the root of your project.

$ bundle install --path=.bundle

Restart your server. Now you are using the local copy.

Gems are loaded in memory

If you're used to make changes to a file and reload the page in your web browser to see the changes live, you might be disappointed when doing changes to ActiveSupport. When the server launches, it loads all the gems into memory. So if you want to debug something in your gem, you will have to reload your web server every time. It can be annoying.

Tests reload the environment every time

Running rake test will reload rails into memory every time. This can be more useful than restarting rails every time you make a change to a gem. Running a single test with no fixture can be very quick to run.

Example

You may know this already, but if the thoughts of opening rails up still gave you the creeps, You are not alone. So here's an example of a debugging session I had last week.

I had to debug a new project using devise and rails 3. I wanted to use TokenAuthentication to let a user log in with a token using the same form someone with an email/password would.

My idea was that if there's no password entered, it should try to match whatever is in the email field with a valid token.

Devise setup

class DeviseCreateUsers < ActiveRecord::Migration def change # Token authenticatable t.string :authentication_token # Database authenticatable t.string :email, :null => false, :default => "" t.string :encrypted_password, :null => false, :default => "" end end

class User < ActiveRecord::Base devise :database_authenticatable, :token_authenticatable end

MyApp::Application.routes.draw do devise_for :users, :controllers => { :sessions => "sessions" } #... end

Now the User model has all the right attribute and method to use either email/password combinaison or token authentification. I also created a route with a custom sessions controller. My reasoning was that if something needs to be customized, it would be in the log in process which is done via Devise::SessionsController. Here's my customized controller (in app/controllers/)

class SessionsController < Devise::SessionsController end

Nothing much is going on here right now! If everything is fine, you should be able to login with devise using your email and password.

Figuring things out

My goal is to have token authentication through the same form as email/password. When I don't really know where to start looking, I browse the source code. Here I want to browse in devise's code to find a way to achieve my plan. Token_authenticatable.rb is a good place to start. I want to know what is going on there before doing any changes. Two methods cross my eyes: token_authenticatable? and valid_for_token_auth?. Looking at the latter made me realise that valid_for_token_auth? calls token_authenticatable?. I guess that if I can make valid_for_token_auth? return true, my goal will be achieved.

Bundle install --path=.bundle

No time for guessing here. I want to know what these methods do and which returns false so I can focus on them.

$ bundle install --path=.bundle

Customize devise

I open up vim and changed a few lines to make sure I don't create exceptions. I don't really care at this point if my modifications are robust. I just want to know what is going on under the hood. Devise is located at .bundle/ruby/1.9.1/gems/devise-2.x.x/.

require 'devise/strategies/base' module Devise module Strategies class TokenAuthenticatable < Authenticatable #Stuff... def valid_for_token_auth? validations = [ token_authenticatable?, auth_token.present?, with_authentication_hash(:token_auth, token_auth_hash) ] p validations validations.reduce :& end # More stuff .. # Extract a hash with attributes:values from the auth_token def token_auth_hash return {} if auth_token.nil? #Added this to prevent exception request.env['devise.token_options'] = auth_token.last { authentication_keys.first => auth_token.first } end end end end

N.b. token_authenticatable.rb is located at lib/devise/strategies/

Logging in with a valid user will print [true, false, false]. First thing first. auth_token is pretty basic to make it return true. ActionController::HttpAuthentication::Token.token_and_options(request) is a Rails component. Here I could either dig in the code of ActionPack and try to fix it. I usually do some reading beforehand as it helps me understand the bigger picture. First of all, the module HttpAuthentication is not easy to find. Let's not dig in the code forever.

% grep -R "module HttpAuthentication" . ./ruby/1.9.1/gems/actionpack-3.2.13/lib/action_controller/metal/http_authentication.rb: module HttpAuthentication

Now I know where to look. Token_and_options(..) proves to be more challenging that I expected. It looks at a header and returns its value. Unless I use javascript(XHR) to forge a query, I won't be able to pass the token. Here I have two solutions that comes to mind:

  • Monkey patching devise's auth_token or;
  • Use a before_filter in my sessions controller to force a header on my request before devises looks for it.

The former can brings loads of issue, updating gems can break my whole setup. I don't really like it. The second sets something that would otherwise be impossible when dealing with normal http forms. That's what I'll do.

My solution

Remember that empty SessionsController? I was lucky since it's exactly where I need to make my customization.

class SessionsController < Devise::SessionsController before_filter :set_token_auth_if_no_password, only: :create protected def set_token_auth_if_no_password resource = params[resource_name] return if resource.nil? || resource[:email].blank? if resource.fetch(:password, "").blank? request.send(:env)["HTTP_AUTHORIZATION"] = "Token token=#{resource[:email]}" end end end

Now, I restart my rails server, log in and check the output for [bool,bool,bool]. It returned [true, true, true]! Could that be it?

I log out and enter my token as my email address leaving the password empty.

I successfully logged in!

If you liked this post, you can follow @pothibo on Twitter.

Get more ideas like this to your inbox

You will never receive spam, ever.