The Facebook Connect JavaScript does some really neat things to function the way it does, but one of its behaviors is unexpected and downright buggy. When attempting to establish a cross-domain communication channel in Internet Explorer (IE8, at least – perhaps not the others), it loads a duplicate copy of the current page in an IFrame. That's right – IE8 users attempt to make two hits to the same page for a single view.
This behaviour is entirely undocumented. I mean, go ahead – check the Javascript SDK – you won't find any mention of it. It's a really bad behavior, too. If you have Facebook Connect (or a Like button, for that matter) on any page that has some transient state (say, a CAPTCHA that gets invalidated and regenerated every time the page loads), loading the page a second time can invalidate the version that the user actually interacts with.
This was the case for our production user registration flow for a long time. The first hit displayed an image for one CAPTCHA, and the second hit surreptitiously invalidated it so that there was no possible way the user could solve the CAPTCHA. Thanks, guys.
The reason for this double hit is straightforward. The same-origin policy restricts how scripts can communicate with different domains. For the most part, interesting interactions are restricted. The whole idea of Facebook Connect is predicated on the idea that their JavaScript will run on your site and provide you with functionality from a different domain, so they had to find a way to bypass the restrictions. There are, quite naturally, some tricky ways to make things work using IFrames. But following the ever-present JavaScript mantra, Not All Browsers Will Behave the Same Way. I actually doubt whether browsers behave at all, ever.
It seems that Facebook's engineers found IE8 wouldn't do their bidding
unless the communication channel IFrame was hosted on the same domain
as the main site. Oh, and that IFrame needs to run their JavaScript,
too. So what better way to ensure this than, uh, reload the current
page (which we know has the Connect JavaScript) in a hidden
IFrame. No site is going to care if they rack up double hits from a
large percentage of their user base, right? As they helpfully append
fb_xd_fragment
as a query parameter, you can just make a special
case short-circuit in your routing based on that. Right? Right?
Remember how I linked to those oh-so-helpful JavaScript SDK docs that had nothing whatsoever to say about this problem? They are (shock of all shocks) incomplete. Littered all over the Internet, you'll find references to a mythical Cross Domain Communication Channel file that solves this problem. Stale versions of Facebook's own documentation and connect-js Github page hint at it, although the current versions of both are devoid of references.
The channel file is a static chunk of HTML hosted on your domain that does nothing but load the Facebook Connect JavaScript. Through a now-undocumented configuration parameter, you can tell the Connect JavaScript on your site to load that file in the IFrame instead of loading a copy of your current page. Problem solved!
Why wouldn't Facebook document this? Oh, wait – they did... sort
of. It's listed as part of
the old JavaScript API.
Oh, but wait. Apparently the API has changed a little, and
xdChannelUrl
should just be channelUrl
. They're close enough,
right? That's like, hardly worth mentioning. Or documenting.
Update 2010-11-26 09:05: It appears this is documented, or at least it is now. Peperone23 points out the FB.init documentation.
So now you do something like this:
FB.init({
apiKey: 'OMGWTFBBQ',
status: true,
cookie: true,
xfbml: true,
channelUrl: window.location.protocol + '//example.com/xd_receiver.html'
});
And in xd_receiver.html, you do this:
<!DOCTYPE html>
<html><head><title></title></head><body><script src="//connect.facebook.net/en_US/all.js"></script></body></html>
Voilà! Facebook can use this static, lightweight, stateless page to achieve their ends instead of loading the current page a second time. Set some way-in-the-future cache headers on that sucker and call it good.