Hypsometry. Modal Synthesis.

On the hinterlands, OpenID, cookies, jars, slippery beasts, indifference, patches of monkeys, and the difference between a general sort of nothing and nothingness in particular.

More news from the poorly mapped hinterlands of Railsia.

In our new app, we’re allowing people to sign in using OpenID. Which is cool, and progressive, and generally a Good Thing. Except for the bit that implementing it’s a nuisance.

Danny’s fully on top of the technical side, which eludes me to a surprising degree. But that’s okay, because the interface side is plenty complex enough to keep me up late. Today I’ve been working on finessing the details of the actual sign in flow, focusing on what happens when things go wrong.

We’ve got this combination sign in page with a tabbed interface, where you can sign in using either a password or an OpenID URL. Two forms, one per sign in option, although you only see one at a time.

If you try to sign in using an OpenID URL, we save that URL in a cookie called signin_openid_url. When the page is rendered, a helper method checks to see if the signin_openid_url cookie exists, and, assuming it does, renders the page such that the OpenID sign in form is visible.

Which form is displayed is determined by which form has .active applied to it. Using CSS rules like these:

#content form.content_tab
{
display: none;
}

#content form.content_tab.active
{
display: block;
}

Which form has .active applied to it is determined by the helper that checks the value of the signin_openid_url cookie. Here’s where we get to the hinterlands of Rails.

The helper we wrote initially was this:

def prefer_openid_signin?
!cookies[:signin_openid_url].nil?
end

Pretty simple: Ask for the cookie identified by :signin_openid_url, check to see if it’s nil?, and return the inverse of that answer.

And it worked most of the time. Unfortunately, it only worked most of the time.

When you enter an invalid URL and submit the form, that URL gets stuck into the cookie and all is well. When you enter no URL, however, and submit the form, things go wrong. prefer_openid_signin? was returning false, even though the signin_openid_url cookie should have been set.

Examining the parameters that were being passed, and the cookies that were being created, I could see that params[:openid_url] => "" when the empty URL form was submitted. This is as it should be. According to Rails dogma, empty text fields should yield empty strings, not nil values.

Then the signin_openid_url cookie was being created, with the correct value of "". But when I asked for the value back again, using cookies[:signin_openid_url], I got nil instead of "". Clearly, the distinction between an empty string and nil was being lost somewhere along the way.

The loss starts with CGI::Cookie, which is the Ruby Standard Library’s HTTP cookie class. CGI::Cookie always returns an Array when asked for the value of a cookie. Always. No matter that most cookies have one and only one value.

Then, in its parsing of the values sent by a client to the server, CGI::Cookie manipulates things such that any empty strings become nil. To be more precise, when a client sends a cookie with an empty string as its value to the server, CGI::Cookie turns that value into an empty Array.

ActionController.cookies uses a container to hold the cookies created by CGI::Cookie. The container’s class is CookieJar, and CookieJar behaves mostly like a Hash. But not quite. More on that in a second.

Anyway, when you ask CookieJar for the value of a cookie: CookieJar checks the size of the Array that CGI::Cookie has given it, and if it contains more than one value you get the first value. But if it contains one value, or none, you get the value of what it contains. So if it contains no values, then the Array is empty, and CookieJar returns nil.

This is contrary to what the documentation in the source says, which is that nil is returned “if no such cookie exists”. Rather, nil is returned if no such cookie exists or if such a cookie exists but contains no value.

So it seems that cookies on Rails are slippery beasts. As I said above, a CookieJar behaves mostly like a Hash. The way in which it doesn’t is that it redefines the setter and the getter methods. Which borders on perverse, in my opinion. The reason for the perversity is that it needs to handle both the setting of outgoing cookies and the getting of incoming cookies. So the interfaces for the two actions are necessarily different. However, that still doesn’t explain why they chose to combine the two different actions into one pseudo-Hash is beyond me. Why not create a new, appropriate, non-confusing interface?

Double anyway. Even though getting and setting cookies in a CookieJar doesn’t work like a Hash, you’d think it would work like a Hash in other regards. You can’t rely on it to return the values you give it, but you’d think it would be able to tell you if it has a key in it or not.

I’d think. Or I thought, so I rewrote the prefer_openid_signin? helper to just check the CookieJar for the presence of the relevant key. (Which corresponds to the existence of the relevant cookie.) As follows:

def prefer_openid_signin?
cookies.has_key?(:signin_openid_url)
end

No. No luck. Bah. Back to the source.

CookieJar, unlike every other use of Hash in Rails, is not a HashWithIndifferentAccess. Which means that if you give it a Symbol as a key when setting a value, you must use the same Symbol to get the value back. And if you use a String to set, you must use a String to get.

But that’s okay, right? Since my OpenID URL cookie creating helper looks like this:

def remember_signin_openid_url(url)
cookies[:signin_openid_url] = { :value => url, :expires => 10.years.from_now }
end

The helper sets the cookie using the Symbol:signin_openid_url, so the other helper should be able to get it using the same Symbol. Right?

Wrong. CookieJar only uses Strings as keys. If you pass it a Symbol, it converts it to the corresponding String, and never looks back. Symbols not welcome here.

Okay, got it. So I rewrote the helper as follows:

def prefer_openid_signin?
cookies.has_key?("signin_openid_url")
end

Ah. Which totally works.

But that’s annoying. I don’t want to have to remember that dealing with cookies is the one time on Rails when I need to care about whether I’m using a Symbol or a String. (And I’ve been bitten by this same, um, quirk when testing cookies.)

Thank goodness for monkey patching. I wrote the following:

module ActionController
class CookieJar < Hash
def has_key?(key)
super(key.to_s)
end
alias :contains? :has_key?
end
end

And put it in lib/cookie_jar.rb, and required it in the appropriate initializer.

Now I can check for the existence of a cookie by checking for the existence of the corresponding key, or in more elegant fashion by asking cookies.contains?(:signin_openid_url). That’s still one more hoop than I really should have to jump through, but at least it’s only one.

No Comments, Comment

Reply to “On the hinterlands, OpenID, cookies, jars, slippery beasts, indifference, patches of monkeys, and the difference between a general sort of nothing and nothingness in particular.”