Saturday, July 21, 2012

Asynchronous loading Facebook Connect...

One of the issues you might encounter in using Facebook Connect is simply the load times for this file. After all, how long does it really take to load the all.js file hosted on their CDN servers?
<script type="text/javascript" src="https://connect.facebook.net/en_US/all.js"></script>
The Facebook docs suggest the following approach for loading all.js, which leverages the HTML5 async attribute to allow other JavaScript code to be loaded and executed in parallel.  (Also notice that the fb-root <div> element is inserted before any Facebook Connect JavaScript code is executed, mostly because the code depends on the presence of such a DOM element to insert its cross-domain handling code.)
<div id="fb-root"></div>
<script>
  window.fbAsyncInit = function() {
    FB.init({
      appId      : 'YOUR_APP_ID', // App ID
      channelUrl : '//WWW.YOUR_DOMAIN.COM/channel.html', // Channel File
      status     : true, // check login status
      cookie     : true, // enable cookies to allow the server to access the session
      xfbml      : true  // parse XFBML
    });

    // Additional initialization code here
  };

  // Load the SDK Asynchronously
  (function(d){
     var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
     if (d.getElementById(id)) {return;}
     js = d.createElement('script'); js.id = id; js.async = true;
     js.src = "//connect.facebook.net/en_US/all.js";
     ref.parentNode.insertBefore(js, ref);
   }(document));
</script>
The proposed solution in the Facebook docs work fine, but what if you have cases in your pages that depend on the use of the FB object (i.e. to check login status of a user)?  The fbAsyncInit() function is called within the Facebook Connect code after everything is setup and calls made within this function will guarantee the existence of the FB object.   If you have other JavaScript code executed in different parts of your app, you'd have to override the window.fbAsyncInit and possibly duplicate a lot of the initialization code, since there is no way to bind multiple handlers to the same event.

In addition, you might also think that putting code within the $(document).ready() handler would provide adequate time for the all.js to be fully loaded, but the browser can signal that the DOM has fully been loaded before the JavaScript itself has completely executed.  In other words, specifying JavaScript <script> tags at the top of your HTML document doesn't guarantee that the JavaScript will be executed before the rest of the DOM has been parsed by the browser.   The net effect is that you can have race conditions, in which the browser throws "'FB' is undefined" errors when you attempt to make Facebook Connect loaded.

One phenomenon I noticed is that jQuery could be consistently loaded before the fbAsyncInit() function was ever called.   This might best be explained by peeking at the internals of the all.js file, since the code is currently structured to execute 6910 lines before the window.FB object is even available. In addition, a lot of internal JavaScript objects are created too and making FB.init() calls often result in network calls to Facebook's OAuth servers to determine login state.  The implication is that we could safely assume that jQuery library was available to us and allow us to create custom events with multiple handlers:

window.fbAsyncInit = function() {
    FB.init({apiKey: [YOUR FACEBOOK APP ID]
             oauth: true,
             channelUrl : [CHANNEL_URL]
             cookie: true
            });

    if (jQuery) {
        jQuery(document).trigger('fbAsyncInit');
    }
   };

In other parts of code, we simply create custom bind handlers and attach Facebook-specific code:

   $(document).bind('fbAsyncInit', function () {
     FB.getLoginStatus(function (response) { });
   });

By using this approach, we've been able to sidestep these FB undefined errors we observed and allow different parts of our application to run FB-specific code. Facebook should probably allow multiple post-init handlers, but in the interim, you may need to adopt a similar approach to avoid such load race conditions.

Saturday, July 7, 2012

Facebook, please more transparency...

On 6/20, I filed a bug to Facebook about an issue with all Facebook Connect-enabled sites using IE7 with Flash getting a blank dialog screen after authenticating. If you disabled Adobe Flash, then there were no problems and you didn't see the fatal blank screen of white. The problem was so consistent that I really didn't think it required more information, but I still uninstalled Flash 11 and installed Flash 10 just to make sure it wasn't an issue with newer Adobe Flash versions with IE7.   (For more background about how Facebook Connect works, see this article.)

The response from the Facebook triage team? Need more info. To try to make a point, I suggested that all you had to do was deminify their all.js JavaScript code, disable the HTML5 'postmessage' handler, and then incorporate this modified all.js in your site in lieu of theirs to reproduce the behavior across all browsers, not just IE7. The irony was that I just noticed that this source code suggestion was removed in the "need more info" section of my bug post.

The section in all.js that deals with registering different cross-domain handlers has a function called isAvailable() to determine whether to use it.  For IE7 browsers (but not IE8+), this return value will evaluate to false.  If you want to leverage other browsers with better debugging tools, the trick is to force a return false for all attempts to register the 'postmessage' handler.  By disabling the HTML5 postmessage code, the JavaScript will revert to using Adobe Flash and implement a custom postMessage() function that mimics the same behavior of the HTML version.

            r.register('postmessage', (function() {
                var s = false;
                return {
                    isAvailable: function() {
                        return false;  /* INSERTED line here */
                        return !!p.postMessage.

Almost 2 weeks went by before I noticed that IE7 logins all of a sudden started working again. What was peculiar was that on 6/3/2012, I noticed that the xd_arbiter.php version got bumped:

Changed file all_deminified.js


1 /*1341364990,169916611,JIT Construction: v585470,en_US*/
1 /*1341373700,169938038,JIT Construction: v585470,en_US*/
22
33 window.FB || (function() {
44     var ES5 = function() {
578578             }
579579         });
580580         __d("XDConfig", [], {
581             "XdUrl": "connect\/xd_arbiter.php?version=8",
581             "XdUrl": "connect\/xd_arbiter.php?version=9",
582582             "Flash": {
583583                 "path": "https:\/\/connect.facebook.net\/rsrc.php\/v1\/ys\/r\/WON-TVLCpDP.swf"
584584             },

You can actually download the files to see what versions 8 and 9 did.  There was a specific change that particularly interested in me:

wget "http://static.ak.facebook.com/connect/xd_arbiter.php?version=8" -O v8.txt
wget "http://static.ak.facebook.com/connect/xd_arbiter.php?version=9" -O v9.txt
$ diff v8.txt v9.txt 
516,517c520,521
<                 var da = z.apply(u.context || a, aa);
<                 if (da) u.exports = da;
---
>                 var ea = z.apply(u.context || a, ba);
>                 if (ea) u.exports = ea;
642c646
<         "path": "https:\/\/s-static.ak.fbcdn.net\/rsrc.php\/v1\/ys\/r\/WON-TVLCpDP.swf"
---
>         "path": "https:\/\/connect.facebook.net\/rsrc.php\/v1\/ys\/r\/WON-TVLCpDP.swf"
743c747,751

Normally a change from fbcdn.net to facebook.net doesn't matter, except that the .SWF file appears to have a check to make sure the file is hosted on a connect.facebook.net site.  If it isn't then the initialization code isn't executed and the postMessage receive handler may not work correctly, which means that successful logins will just remain stuck and not proceed to the next step.  You can review the Flash code by using showmycode.com and decompile the .SWF file listed above.

            if (_local3 != "connect.facebook.net"){
                XdComm.fbTrace("XdComm is not loaded from connect.facebook.net", {swfDomain:_local3});
                if (_local3.substr(-13) == ".facebook.com"){
                    _local4 = PostMessage.extractPathAndQuery(_local2);
                    if (_local4.substr(0, 8) != "/intern/"){
                        XdComm.fbTrace("XdComm is NOT in intern mode", {swfPath:_local4});
                        return;
                    };
                    XdComm.fbTrace("XdComm is in intern mode", {swfPath:_local4});
                } else {
                    return;
                };

Someone must have recently noticed the problem and quietly fixed it.  This change may have also explained why I started noticing a bunch of security exceptions when attempting to use the Flash v10 debugger version when logging in via IE7.  There were also a bunch of debugging messages in the JavaScript console that indicated that the SWF files were being initialized correctly except that all messages being sent were not being acknowledged by the postMessage receive handler.  


I'm not sure what could have been done except to point out to Facebook exactly what needed to be fixed.  Regardless, Facebook, please be more transparent about these issues, since this incident is not the first time you've broken Facebook Connect IE7 logins and your API platform status pages rarely advertise these issues.