Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Internet Explorer Will Be Stuck When Requests Too Much? Fake the XHR!

2.57/5 (3 votes)
21 Jun 2007CPOL5 min read 1   177  
Internet Explorer will be stuck when too many connections have been set up in the page at the same time. Let's build a fake XHR object to solve the bug for Internet Explorer browser.

Introduction

Months ago, a friend of mine who is also a consultant and trainer, told me that one of his customers met with a problem. Internet Explorer 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 "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 another page, the browser has to wait for running calls to complete before it can take another one. The bug is quite serious in Internet Explorer 6 and unfortunately, it still exists in Internet Explorer 7.

Manage the Requests Programmatically

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:

JavaScript
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 a delegate into the queue. The delegate will be executed when there's none 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:

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

We'll use the ConnectionManager to queue the requests as follows:

JavaScript
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 fail 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 several 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. It will be perfect if we can use another component to replace the native XMLHttpRequest object and provide the built-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 following code has solved the compatibility problem of XHR in different browsers:

JavaScript
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 _originaActiveXObject(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 _originaActiveXObject(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.

JavaScript
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.

JavaScript
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 takes 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 be executed automatically from the devs' point of view.

JavaScript
_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 behave the same as the native XHR type. But it still exists. It is a little thing but 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 receive the headers from server side. But in Internet Explorer, 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 using 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 Internet Explorer.

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

I sent the script file to my friend. It seems that his customer is quite pleased with this solution.

History

  • 21st June, 2007: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)