November 15, 2007

My First Monkey Patch for a Rails routes.rb bug

For the past five days I have been struggling with a bug in the route code in rails 1.2.3. I finally monkey patched url_for to essentially force it to do the obvious.

In this blog post, I describe the bug and how I reproduced it and then the patch to work around the bug.

There is a 10% chance that the routes is actually working and I just do not understand it. I read in the Agile book something about routes trying to use the shortest URL where possible - but the problem is that I have these long URLs to pass bits of session-like information around between my ajax calls - particularly when I want to go cross domain with my Ajax (i.e. I want to use a Rails tool from one server included into a web page from another server where cookies and session information will not be usable).

The bug comes down to when I call something with a long url with lots of stuff defined and then I generate a new url adding an id which is always at the end of the routes, url_for decides to "forget" the stuff that was to the left of it in the incoming request URL. Changing action only generates the *right* url while changing the action and id generates the wrong URL.

My Urls have three basic forms

/imsti.fe828-12345-bfea.1.right-content/comments/ajaxstart
/ajax.1.right-content/comments/ajaxstart
/comments/ajaxstart

The first two are for use in divs and never shown to the user. The imsti one both establishes a session and sets up the context and div name when the tool is being used cross-domain. The second is used when you are not going cross-domain and cookies are available to establish session because the ajax and the enclosing container are coming from the same server - so incoming ajax requests get the cookies.

Here is my routes.rb - the three routes in question are the ones that freak Rails url generation out - even though I never use not generate Urls with the imsti prefix - other URLs are incorrectly generated as you will see in this short sad story.

map.connect '', :controller => "portal"
# Problem causing routes
map.connect 'imsti.:imssession.:context.:div/:controller/:action/:id.:format'
map.connect 'imsti.:imssession.:context.:div/:controller/:action/:id'
map.connect 'imsti.:imssession.:context.:div/:controller/:action'
#
map.connect 'ajax.:context.:div/:controller/:action/:id.:format'
map.connect 'ajax.:context.:div/:controller/:action/:id'
map.connect 'ajax.:context.:div/:controller/:action'

map.connect ':controller/:action/:id.:format'
map.connect ':controller/:action/:id'
map.connect ':controller/:action'

The bug happens when I go to a page with a Url like this

/ajax.1.right-content/comments/ajaxstart

And then in the view for this page I do this:

<%= link_to_remote 'view', :update=> portal_get_div,
:url => { :action => 'view', :id => comment.id } %>

Simple enough - I am changing the action and adding an ID - it should inherit the rest since in all my routes id and action are at the end.

Here is the problem - when I do not have the three imsti routes in my routes.rb all works well - the link_to_remote generates this url:

/ajax.1.right-content/comments/view/1

If I add the three imsti prefixed routes to my routes.rb the same code when called from the same url:

/ajax.1.right-content/comments/ajaxstart

Makes this URL:

/comments/view/1

Aargh! And on the same page in the same view, this code:

<%= link_to_remote 'Manage', :update=> portal_get_div,
:url => { :action => 'manage' } %>

Makes beautiful URLs like this regardless of what version of the routes.rb I use:

/ajax.1.right-content/comments/manage

So what the heck is happening - it works for action and fails when you do action+id - clearly the addition of an :id is triggering some deep stupid behavior in the url_for.

A key here is that I am never calling nor using a route that starts with imsti - the mere adding of the routes causes things to break.

I have played for hours and at one point removed my three routes and had a single route that would cause the evil behavior. I lost the exact single route that broke it - but it looked something like this:

map.connect 'routesbiteme/:controller/:action'

It had an action but no id - and yet it caused url generation to blow its brains out. I need to look at the Rails source code. One day I will download my own copy and start hacking on Rails - but I will save that for when I have more time.

So here is the monkey patch to fix it:


module ApplicationHelper
include Osids
include Portal
protected
# This is inspired by another monkey patch for support in browsers
# without cookies - but since cookie_only >= false seems broken
# that patch while useful to read is not much help
#
# http://www.edgesoft.ca/blog/read/2
#
# Funny - sometimes options is a string - in this view code
#
# <% form_remote_for :umap, @user, :update=> portal_get_div,
# :url => { :action => "add" } do |f| %>
#
# options comes in as a string like this:
#
# /ajax.1.main-content/users/add
#
# I am sure this is yet another bug. But we need to simply
# not patch the Hash unless it is really a hash
#
def url_for(options = {}, *parameters_for_method_reference)
if options.class == Hash
if options[:div] == nil && params[:div] != nil
options[:div] = params[:div]
end
if options[:context] == nil && params[:context] != nil
options[:context] = params[:context]
end
if options[:imssession] == nil && params[:imssession] != nil
options[:imssession] = params[:imssession]
end
end
super
end
end

Effectively if the parameters are on the request, and not in options, I force them into options. Somehow this convinces the recalcitrant url generation to really see the parameters.

I wasted about 15 hours on this one - I guess I learned something from it. I cannot wait to see the ugly code in url generation or secret undocumented back-door feature that broke this.

P.S. I am still struggling with how to set the Rails session id from a URL - my other reason to be peeved at the Rails folks - don't get me started on cookie_only and Rails developers rushing to put in security fixes without thinking about real impact. For now I have a hack to do IMS TI sessions in my own framework without using Rails sessions - to get on to cross-domain Ajax testing.

Posted by csev at November 15, 2007 08:41 AM