AJAX Polling

⚠️ This article was originally published in 2005 at dubi.org/ajax-polling. The content is extremely outdated and is preserved here for nostalgic purposes only.

Yes we know that there have been a lot of AJAX tutorials as of late, but we thought we would share some of our experiences and try to explain some things that other tutorials might have left out.

We found that while other tutorials cover the very basic methods for using xmlhttprequest, they failed to explain how to use them in a practical manner, especially in applications that need to poll the server in quick succession. When we started work on Ramble, an open source AJAX chat client, we ran into a lot of these problems. After a lot of trial and error, we’ve come up with a good way to do quick successive requests and deal with all of the issues surrounding cross-browser compatibility.

Quick successive requests

Problem

If you’ve ever tried to write an application that uses the xmlhttprequest to constantly get information as opposed to a one shot deal, you’ve probably noticed that it often behaves erratically. Sometimes the data won’t come through properly or it will work properly for a while and then just stop completely. What it comes down to is the fact that the xmlhttprequest object is really just good for about one request.

Solution

The best way we found to deal with the erratic nature of the xmlhttprequest is to actually use one object per request. So basically any time you make a request create a new object. In all of our testing, this really doesn’t add much overhead and you can easily make requests to a server every 100ms if you wanted. An easy way to handle this is to just make a function that takes care of creating the object and then call that every time you want to make a request.

Here is an example:

function makeRequest() {
var xmlhttp = createXmlObj(callback);
var url = "XMLGenerator.php";
xmlhttp.open("GET", url, "true");
xmlhttp.send(null);
}
function createXmlObj(callback) {
var XmlObj;
// IE-specific code would go here as well
XmlObj = new XMLHttpRequest();
if (callback) {
XmlObj.onreadystatechange = function() { ... }
}
return XmlObj;
}

Using this method of recreating the object every time you make a request should greatly reduce problems in your application. Another way to make sure you’re application works correctly is to properly handle exceptions.

How to handle exceptions

It is very important that you put all of your requests in try-catch blocks. If you don’t and an exception occurs your application will stop working. When your code is in a try-catch block, you can easily handle the exceptions yourself and recover gracefully.

The easiest and most effective way to handle exceptions is to just scrap the request that caused the exception, recreate the xmlhttprequest object and try again. This will make sure that you don’t use data that is corrupt as well as allow your application to keep running without the user noticing anything unusual.

Here is an example:

try {
xmlhttp.open("GET", url, "true");
xmlhttp.send(null);
}
catch(e) {
setTimeout("makeRequest()", 2000);
}
}

As you can see, it’s pretty easy to handle exceptions and you should do it anywhere you make a request.

Cross-browser Issues

The only non-trivial differences we encountered were:

  1. Initializing the xmlhttprequest object
  2. Safari’s response bugs
  3. Differences in parsing the XML response

The first is obviously no big deal and you can look at the code to see how it is initialized.

The second deals with Safari occasionally returning an invalid status code. These invalid responses seemed to coincide with requests that were made that used cached versions of the response. Of course, the status code should still be 200 (OK) if it is using a cached version, and otherwise this would be a minor bug, but the problem here is that Safari is ignoring the header of the response page, which is telling it to not cache. On the PHP end, we are setting the appropriate header values which Firefox and IE both handle properly, but Safari ignores. So, this is a two-fold problem: Safari is caching the page when it shouldn’t, and Safari is not setting the status code to 200 on these instances. Fortunately, this is easily fixed:

xmlhttp.open("GET", url, "true");
xmlhttp.setRequestHeader("If-Modified-Since", "Wed, 15 Nov 1995 00:00:00 GMT");
xmlhttp.send(null);

Manually setting the request to ignore cached copies if more recent than Nov 15, 1995 does the trick.

(Note that we are breaking standards here by using GET to make the requests and forcing the browser to not used cached copies of the code. We should be using POST since GET should be idempotent and not care whether the user decides to grab a cached version or not. Fortunately, we don’t care.)

Random Tips

  1. Don’t forget to set the content-type to text/xml in whatever scripting language you’re using to generate the responses (e.g. in PHP: header(“Content-type: text/xml”);)

  2. JavaScript has multiple url-encoding functions for strings. To get the correct effect we had to:

// escape backslashes
message = message.replace(/\\/, "\\\\");
// encodeURIComponent the parameter
url = "postMessage.php?msg=" + encodeURIComponent(message);
  1. If you’re polling every 100ms or anything very fast, make sure you can handle that many requests, and make whatever is receiving the requests as lean as possible. Don’t make 4 fulltext SQL queries every time you receive a request or be prepared to scrape your server off the ground.

  2. Don’t sniff the browser every time you fork code off for IE. Do it once, do it early, and store it:

var agt = navigator.userAgent.toLowerCase();
var isIE = agt.indexOf("msie") != -1 && agt.indexOf("opera") == -1;
  1. Don’t panic.

Demo and Source Code

Below you can browse the example code demonstrating these concepts. You can also download the source code. Feel free to use the source code in any way you wish.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" >
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" >
<head>
<title>XMLHttpRequest Polling</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<script language="JavaScript" src="ajax_poller.js"></script>
</head>
<body onLoad="init()" style="font-family: Courier, Courier New, mono;">
The constantly changing random string from hell!
<p id="randomString">aosdhfpuoahwepfoiauhwpoeuihnapuwhntpciuhnrcaosdhfpuoahwepfoiauhwphfpuoahwentpciuhnrc</p>
<p># of requests: <span id="requestCounter">0</span></p>
<p># of time-outs: <span id="timeoutCounter">0</span></p>
<p>(with 100ms forced delay between requests)</p>
<p>Time to make last request: <span id="requestTime">0</span>ms</p>
<p>Average request time: <span id="averageTime">0</span>ms</p>
</body>
</html>
var timeoutTimer; // we'll use this to catch timeouts
var requestCounter; // # of requests we've made (loops back to 0 after 30k requests)
var lastTime;
var totalTime;
// Executed at start of application (body onLoad)
function init() {
timeoutTimer = false;
requestCounter = 0;
lastTime = new Date();
lastTime -= 100;
totalTime = 0;
// start polling
makeRequest();
}
// Loops infinitely, trying to make requests
function makeRequest() {
// calculate times
var curTime = new Date();
var requestTime = curTime - lastTime - 100; // account for 100ms delay
document.getElementById("requestTime").innerHTML = requestTime;
// calculate averages
totalTime += requestTime;
document.getElementById("averageTime").innerHTML =
Math.round( totalTime / requestCounter * 100 ) / 100; // round to two decimal spaces
// reset clock
lastTime = curTime;
if (requestCounter > 30000) {
totalTime = 0;
requestCounter = 0;
}
else {
requestCounter++;
}
document.getElementById("requestCounter").innerHTML = requestCounter.toString();
// XMLHttpRequest obj not re-usable?
var xmlhttp = createXmlObj(updateString);
// Let's get the ball rolling... [re]start the application
var url = "characterGenerator.php";
// every 100 requests, do something
if (requestCounter % 100 == 0) {
url += "&doSomething_a=1";
}
// every 99 requests, do something else
if (requestCounter % 99 == 0) {
url += "&doSomething_b=1";
}
// start timeout timer; goes off if no response in 5 seconds
if (timeoutTimer != null) {
clearTimeout(timeoutTimer);
}
timeoutTimer = setTimeout("startTimeoutProcedure()", 5000);
try {
xmlhttp.open("GET", url, "true");
// safari fix
xmlhttp.setRequestHeader('If-Modified-Since', 'Wed, 15 Nov 1995 00:00:00 GMT');
xmlhttp.send(null);
}
catch(e) {
// Sending occasionally fails. Restart when this happens
// try again in 2 seconds
setTimeout("makeRequest()", 2000);
}
}
// Takes a responseXML obj of chars and update the random string!
function updateString(xmlDoc, xmlhttp) {
var characters = xmlDoc.getElementsByTagName("characters");
var stringEl = document.getElementById("randomString");
var curString = stringEl.innerHTML;
var randNum = Math.round((Math.random()*(curString.length-5))); // which characters to update
var childNode, chr, newString;
if (characters != null && characters.length != 0) {
characters = characters.item(0);
for (var i = 0; i < characters.childNodes.length && i < 5; i++) {
curString = stringEl.innerHTML;
newString = curString;
childNode = characters.childNodes.item(i);
// skip non element_nodes
if (childNode.nodeType != 1) continue;
if (childNode.nodeName == "char") {
chr = childNode.childNodes[0].nodeValue;
}
// replace the char
newString = curString.substr(0, randNum);
newString += chr;
newString += curString.substr(randNum+1);
randNum++;
// update
stringEl.innerHTML = newString;
}
}
delete xmlhttp;
}
// every 5 seconds, make another request if no response is recieved
function startTimeoutProcedure() {
var timeoutCounter = document.getElementById("timeoutCounter").innerHTML.toString();
document.getElementById("timeoutCounter").innerHTML = ++timeoutCounter;
makeRequest();
}
// Creates the XMLHttpRequest object and resets any variables
// needed to restart the application
// Takes a single argument: the callback function (null for no callback)
function createXmlObj(callback) {
var XMLObj;
/*@cc_on @*/
/*@if (@_jscript_version >= 5)
// JScript gives us Conditional compilation, we can cope with old IE versions.
// and security blocked creation of the objects.
try {
XMLObj = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e) {
try {
XMLObj = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (E) {
XMLObj = false;
}
}
@end @*/
if (!XMLObj && typeof XMLHttpRequest != 'undefined') {
XMLObj = new XMLHttpRequest();
XMLObj.overrideMimeType('text/xml');
}
if (callback) {
XMLObj.onreadystatechange = function() {
// every time there is a change in readyState, reset the time-out timer
if (timeoutTimer != null) {
clearTimeout(timeoutTimer);
}
timeoutTimer = setTimeout("startTimeoutProcedure()", 5000);
try {
if (XMLObj.readyState == 4) {
// send response to the callback function
callback(XMLObj.responseXML, XMLObj);
// make another request in 100 ms (so polling interval is request time + 100ms)
theTimer = setTimeout("makeRequest()", 100);
}
}
catch(e) {
}
}
}
return XMLObj;
}
<?php
// We don't want this page to get cached, so let's throw a bunch of crap at the header
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // always modified
header("Cache-Control: no-store, no-cache, must-revalidate"); // HTTP/1.1
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache"); // HTTP/1.0
// return an XML document (comment this out if you're not returning XML)
header("Content-type: text/xml");
// Do something every 100 requests
if (isset($_GET['doSomething_a'])) {
}
// Do something else every 99 requests
if (!isset($_GET['doSomething_b'])) {
}
// Generate 5 random characters
echo '<characters>';
for ($count=0; $count < 5; $count++) {
$chr = chr(!mt_rand(0,2)?mt_rand(48,57):(!mt_rand(0,1)?mt_rand(97,110):mt_rand(111,122)));
echo "<char>$chr</char>";
}
echo '</characters>';
?>

<-Find more writing back at https://alan.norbauer.com