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.