IE will be stuck when requests too much? Fake the XHR!

It's one of the articles I wrote in CodeProject, click here to read the original one.

Introduction

Months ago, a friend of mine, who is a also a consultant and trainer, told me that one of his customers met a problem. IE will be stuck when too many connections have been set up in the page at the same time. The problem is becoming more and more popular since AJAX technology has been widely used these days. When an AJAX application is composed of smaller ones - that we call it "mash up" - the problem will be likely to occur.

It's a bug in Internet Explorer. When you make a lot of AJAX calls, the browser keeps all the requests in a queue and executes two at a time. So, if you click on something to try to navigate to anthoer page, the browser has to wait for running calls to complete before it can take another one. The bug is quite serious in IE 6 and unfortunately, it still exists in IE 7.

Manage the requests programically

The solution is simple. We should maintain the queue ourselves and send requests to the browser's queue from our queue one by one. Thus I wrote a queue to manage the requests. It's really a piece of cake:

if (!window.Global)
{
    window.Global = new Object();
}
 
Global._ConnectionManager = function()
{
    this._requestDelegateQueue = new Array();    
    this._requestInProgress = 0;    
    this._maxConcurrentRequest = 2;
}
 
Global._ConnectionManager.prototype =
{
    enqueueRequestDelegate : function(requestDelegate)
    {
        this._requestDelegateQueue.push(requestDelegate);
        this._request();
    },
   
    next : function()
    {
        this._requestInProgress --;
        this._request();
    },
   
    _request : function()
    {
        if (this._requestDelegateQueue.length <= 0) return;
        if (this._requestInProgress >= this._maxConcurrentRequest) return;
       
        this._requestInProgress ++;
        var requestDelegate = this._requestDelegateQueue.shift();
        requestDelegate.call(null);
    }
}
 
Global.ConnectionManager = new Global._ConnectionManager();

I build the component names ConnectionManager using pure JavaScript code without any dependence on any AJAX/JavaScript framework/library. If users want to use this component to manage the request, they should use enqueueRequestDelegate method to put an delegate into the queue. The delegate will be executed when there's no or only one request is running in the browser. And after receiving the response from the server, the user must call the next method to notify the ConnectionManager, and then the ConnectionManager will execute the next pending request delegate if the queue is not empty.

For example, if we are using Prototype framework to make ten AJAX calls continuously:

function requestWithoutQueue()
{
    for (var i = 0; i < 10; i++)
    {
        new Ajax.Request(
            url,
            {
                method: 'post',
                onComplete: callback
            });
    }
}
   
function callback(xmlHttpRequest)
{
    // do sth.
}

And we'll use the ConnectionManager to queue the requests as following:

function requestWithQueue()
{
    for (var i = 0; i < 10; i++)
    {
        var requestDelegate = function()
        {
            new Ajax.Request(
                url,
                {
                    method: 'post',
                    onComplete: callback,
                    onFailure: Global.ConnectionManager.next,
                    onException: Global.ConnectionManager.next
                });
        }
       
        Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
    }   
}
 
function callback(xmlHttpRequest)
{
    // do sth.
    Global.ConnectionManager.next();
}

Please note that we assign the next method to both the onFailure and onException callback handlers to guarantee that it will be called after receiving the response from the server, since the rest delegate in the queue will be failed to execute and the system cannot raise a new call anymore if the next method hasn't been executed.

I send the file to my friend and serveral days later he told me that his customer said the component is hard to use. I agreed. It's really verbose and error prone. Apparently the ConnectionManager is not so convenient to be integrated into the existing codes. The devs must make sure that all the requests should be queued in ConnectionManager and the next method must be executed in any case when the request finishes. But it's far from enough yet. More and more AJAX applications will execute scripts created by the server. Perhaps the dynamically created file cannot be loaded successfully if the internet connection of client side is not stable enough. At that time, the scripts execution throws exceptions and the next method which should be executed by design will probably be missed. 

Build a fake XMLHttpRequest type

I got an idea after days of thinking. That will be perfect if we can use another component to replace the native XMLHttpRequest object and provide the build-in request queue. If so, devs can solve the problem by putting the script file in the page without changing a single line of code.

The solution is much easier than I thought before and now I'm going to show you how to build it.

The first thing we should do is to keep the native XHR type. Please note that the follow code has solved the compatibility problem of XHR in different browsers.

window._progIDs = [ 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP' ];
 
if (!window.XMLHttpRequest)
{
    window.XMLHttpRequest = function()
    {
        for (var i = 0; i < window._progIDs.length; i++)
        {
            try
            {
                var xmlHttp = new _originalActiveXObject(window._progIDs[i]);
                return xmlHttp;
            }
            catch (ex) {}
        }
       
        return null;
    }
}
 
if (window.ActiveXObject)
{   
    window._originalActiveXObject = window.ActiveXObject;
 
    window.ActiveXObject = function(id)
    {
        id = id.toUpperCase();
       
        for (var i = 0; i < window._progIDs.length; i++)
        {
            if (id === window._progIDs[i].toUpperCase())
            {
                return new XMLHttpRequest();
            }
        }
       
        return new _originalActiveXObject(id);
    }
}
 
window._originalXMLHttpRequest = window.XMLHttpRequest;

And then, we should create a new class to replace the native XHR type. Most of the methods are just delegated to the corresponding one in the native object. 

window.XMLHttpRequest = function()
{
    this._xmlHttpRequest = new _originalXMLHttpRequest();
    this.readyState = this._xmlHttpRequest.readyState;
    this._xmlHttpRequest.onreadystatechange =
        this._createDelegate(this, this._internalOnReadyStateChange);
}
 
window.XMLHttpRequest.prototype =
{
    open : function(method, url, async)
    {
        this._xmlHttpRequest.open(method, url, async);
        this.readyState = this._xmlHttpRequest.readyState;
    },
   
    setRequestHeader : function(header, value)
    {
        this._xmlHttpRequest.setRequestHeader(header, value);
    },
   
    getResponseHeader : function(header)
    {
        return this._xmlHttpRequest.getResponseHeader(header);
    },
   
    getAllResponseHeaders : function()
    {
        return this._xmlHttpRequest.getAllResponseHeaders();
    },
   
    abort : function()
    {
        this._xmlHttpRequest.abort();
    },
   
    _createDelegate : function(instance, method)
    {
        return function()
        {
            return method.apply(instance, arguments);
        }
    },
   
    _internalOnReadyStateChange : function() { /* ... */ },
   
    send : function(body) { /* ... */ }
}

The key points are the implementations of the send method and _internalOnReadyStateChange method. The send method will put a delegate of the native XHR type's method into the queue. The delegate will be executed by the ConnectionManager at a proper time. 

send : function(body)
{
    var requestDelegate = this._createDelegate(
        this,
        function()
        {
            this._xmlHttpRequest.send(body);
            this.readyState = this._xmlHttpRequest.readyState;
        });
   
    Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
},

We assign the _internalOnReadyStateChange method as the onreadystatechange callback handler of the native XHR object in the constructor. When the callback function raises, we'll keep all the native properties into our object and execute our onreadystatechange handler. Please note that our new component take the responsibility of executing the next method of ConnectionManager when the readyState equals to 4, which means the current request is "completed", so that the next method can been executed automatically from the devs' point of view. 

_internalOnReadyStateChange : function()
{
    var xmlHttpRequest = this._xmlHttpRequest;
   
    try
    {
        this.readyState = xmlHttpRequest.readyState;
        this.responseText = xmlHttpRequest.responseText;
        this.responseXML = xmlHttpRequest.responseXML;
        this.statusText = xmlHttpRequest.statusText;
        this.status = xmlHttpRequest.status;
    }
    catch(e){}
   
    if (4 == this.readyState)
    {
        Global.ConnectionManager.next();
    }
   
    if (this.onreadystatechange)
    {
        this.onreadystatechange.call(null);
    }
},

We have tried our best to let the new component behaves as the same as the native XHR type. But it still exists an little thing we can't do it. When we access the status property in the native XHR object, an error would be thrown if the object cannot recieve the headers from server side. But in IE, we can't define the object's property as a method like using __setter__ keyword in FireFox. It's the only difference between the native XHR type and our new one when use the two components.

How to use

And now, we can easily reference the js file in the page to solve the problem when the user browses the page using IE.

<!--[if IE]>
<script type="text/javascript" src="ConnectionManager.js"></script>
<script type="text/javascript" src="MyXMLHttpRequest.js"></script>
<![endif]-->

I sent the script files (see attachment) to my friend. It seems that his customer is quite pleased with this solution.

 

4 Comments

  • So the 2-request limit is actually according to the HTTP 1.1 spec. It just so happens that IE enforces it more than FireFox or Safari.

  • Hmm, maybe this explains why I can only combine 2 JSON requests in Internet Explorer while Firefox allows me to run 3?

  • Omar Al Zabir created a global queue manager for ASP.NET AJAX. It's similar to your implementation except that it doesn't use a fake XHR. Good article though!

  • @rrobbins
    What is "combine 2 JSON" requests? Actually the "2 connections limit" will not realize what kind of connections have been estabilished. It means that if there's one connection which is loading image and another one is getting JSON string in AJAX style, the third connection would be "blocked" until one of the above finished.
    And I think firefox is according the spec and it won't open the third connection (to the same domain) simultaneously.

Comments have been disabled for this content.