In my continuing quest to understand XMLHTTP I gathered some very intriguing material that I'm quite sure will save somebody else's ass. Today I offer a closer look at the abort()
method, as well as an as yet unexplained bug in Mozilla which causes the responseXML
to go missing.
A note first of all: I tested everything below in Safari, too, and it hasn't given me a single spot of trouble. It does exactly what I expect it to do, when I expect it. Kudos to the Safari team!
I'm currently working on a very simple XMLHTTP script (I hardly dare call it an application) for a Dutch ISP. When a user enters his postal code, an XMLHTTP request is automatically fired to retrieve data about the download speeds the user can expect when signing up for the ISP's new fast ADSL connection.
Update: This project is now online.
Dutch postal codes look as follows: 1072 VP. Four numbers, an optional space, two letters. That doesn't really matter, but anyway.
In itself the XMLHTTP component is very simple: send postal code and house number to server, and retrieve expected download speed and/or an error message. Show these bits of data in page, start up some nice animations, ready.
The tricky bit is that the client definitely wants the XMLHTTP to start working as soon as the user has entered the second number. The check for this partial postal code is still very rough, and the server side programmer sensibly opted to send back the slowest download speed associated with it. When the user enters more of his postal code and/or his house number, the search is refined.
So far so good. The keyup
event fires the xmlhttp request. (Not keypress
! It fires before the new character has been added to the input field.)
The problem, obviously, is that a user may type so fast, and/or the server may be so slow, that one request overtakes another. User types second number, request goes out, but before it comes back he types the third number, and another request goes out etc. This turns out not to be quite such a problem as I expected, but the writing of this part of the script uncovered quite a few interesting features.
From the outset I decided to use only one xmlhttp object. When the user fires a new request, the old one is (should be) overwritten. Only the data retrieved by the last request counts, the results of earlier requests can be safely discarded.
It's here that the abort()
method comes in. My original plan was to check if the single xmlhttp object was busy downloading something before sending a new request. If it was, I could abort the previous request and send my new one. This plan looked good in theory, and it even sort of works in practice, but while implementing this bit of the script I discovered quite a few things.
First of all: is it necessary to use abort()
? Not really, except in Mozilla.
If you take an xmlhttp object that's busy sending and receiving and tell it to send another request, it simply stops doing whatever it does and sends out the new request. Except in Mozilla.
Error: uncaught exception: [Exception... "Component returned failure code: 0xc1f30001 (NS_ERROR_NOT_INITIALIZED) [nsIXMLHttpRequest.send]" nsresult: "0xc1f30001 (NS_ERROR_NOT_INITIALIZED)" location: "JS frame :: [URL censored] :: zendGegevens :: line 68" data: no]
Since line 68 is the line that does xmlhttp.send(null)
I assume that Mozilla cannot handle the assignment of a new request to an xmlhttp object that's still busy. (I'm guessing. I cannot actually read these confused error messages. It's a safe guess, though.)
So far so bad. abort()
is necessary for Mozilla.
So I added an if
clause that sees if the xmlhttp object is still busy. Originally I tried to set a custom property to the xmlhttp object: xmlhttp.isBusy = true
when I send the request, and xmlhttp.isBusy = false
when the request is ready. Didn't work.
Explorer doesn't allow you to set custom properties on an xmlhttp object.
No problem, I created a global variable isBusy
, which worked fine.
So I wrote this bit of script:
var queryString = [gather data]; if (isBusy) { xmlhttp.abort(); } xmlhttp.open("GET",url+queryString,true); isBusy = true; xmlhttp.onreadystatechange = catchData; xmlhttp.send(null); function catchData() { if (xmlhttp.readyState != 4) return; isBusy = false; [handle data] }
The results were quite odd, and it took me a long time to track down the cause:
When the abort()
method is used, the readystatechange
event fires in Explorer and Mozilla. Worse, readyState = 4
, which means that the average xmlhttp script assumes the data has been loaded correctly. This can give very weird effects.
The solution was to clear the onreadystatechange
event handler before calling abort()
.
if (isBusy) { xmlhttp.onreadystatechange = null; // no go in IE xmlhttp.abort(); }
However, setting onreadystate = null
in the context of abort()
gives a very weird error in Explorer.
The error reads something like "Types don't match"; I'm translating from Dutch here. Therefore in the end I decided to assign an empty function to the event handler:
if (isBusy) { xmlhttp.onreadystatechange = function () {} xmlhttp.abort(); }
Don't ask me why this is necessary, but it works.
responseXML
Then the final, as yet unsolved Mozilla bug creeped up.
Sometimes, even though the xmlhttp request goes fine, there's just no responseXML
. Though this can happen with "intermediate" requests, which are overruled by a following request, it can also happen with the very last request that is fired, one that is not overruled and should work fine.
I confirmed this bug in the new Firefox 1.5 (Beer Park).
Two points merit further attention:
responseXML
goes missing only when the xmlhttp request skips readyState 3
(alternately: readyState 3
doesn't fire when the responseXML
is missing). In all cases 1, 2 and 4 fire normally.abort()
, because when I remove the abort()
bit the bug disappears. Of course, then the xmlhttp.send()
bug I mentioned earlier returns in all its glory. Remember: this bug sometimes occurs with the last, non-aborted xmlhttp request, so it can't be simply a result of aborting the request.That's where I stand now. I have no idea what causes the bug, or how to solve it. In the current situation I have the choice between two bugs. The script won't ever work correctly in Mozilla when the user types very fast and/or the server is very slow.
BTW: while doing research I finally found a clear description of the five ready states at the new developer.mozilla.org. It says:
Value | Description |
---|---|
0 | UNINITIALIZED - open() has not been called yet. |
1 | LOADING - send() has not been called yet. |
2 | LOADED - send() has been called, headers and status are available. |
3 | INTERACTIVE - Downloading, responseText holds the partial data.
ppknote: Can go missing in Mozilla, in which case the responseXML also goes missing.
|
4 | COMPLETED - Finished with all operations. |
Although I doubt whether this is an entirely correct description for all browsers, it's certainly much better than the obscure ones we find elsewhere. Until now I only found descriptions that pompously state "Uninitialized, Loading, Loaded, Interactive, Completed" and leave us unenlightened as to what that actually means.
addEvent()
recoding contestThis is the blog of Peter-Paul Koch, web developer, consultant, and trainer.
You can also follow
him on Twitter or Mastodon.
Atom
RSS
If you like this blog, why not donate a little bit of money to help me pay my bills?
Categories:
Comments are closed.
1 Posted by Shawn Wilsher on 12 September 2005 | Permalink
Do you plan to file a bug report to Mozilla regarding the bug you found? Chances are, if you file it, it would be fixed (eventually). Information for filing a bug can be found here:
http://www.mozilla.org/support/firefox/bugs
Also, it's "Deer Park", not "Beer Park", although the latter is more interesting.
2 Posted by Alex Lein on 12 September 2005 | Permalink
LOL, Beer Park!
I had this problem a few weeks ago, and I also could not find a "solution". I had to re-code a large portion of script to use individual XMLHTTPRequest objects and then track which was the most current object. If it's stupid but works, it's not stupid!
3 Posted by Michael Mahemoff on 13 September 2005 | Permalink
It sounds like you can't assume very much about states 2 and 3, I'm not sure how consistent 1 is either.
http://www.davidflanagan.com/blog/2005_08.html#000078
(see the comments too).
4 Posted by Mark Wubben on 13 September 2005 | Permalink
Couldn't you have used `readyState` instead of `isBusy`? Also, concerning the "Type mismatch" in IE; I assume that when it starts sending it internally checks for an event handler and stores a reference to the variable. Now, when you remove the event handler, that reference doesn't match anymore, resulting in the error. But that's just a theory :)
5 Posted by Alex Collins on 13 September 2005 | Permalink
Slightly off topic, but...
There is a another way of doing input auto complete. Wait until the person stops typing for a short period of time. This means that you don't create a new XML request for each character typed. You'd still need to abort the request when a new character is typed, but youd reduce the over head.
<input onKeyUp="clearTimeout(t); var t = setTimeout('complete(' + this.value + ');', 250)" id="a">
<script type="text/javascript">
var v; // completion value
var xmlHttp; // XML request
function complete(newV) {
// key up may not be a new character, e.g. backspace key
if (newV == v)
return;
// abort request in progress
if (xmlHttp) {
xmlHttp.onreadystatechange = function() {};
xmlHttp.abort();
}
// send XML request ...
}
function readyStateChanged() {
// if not ready, and OK, fill out details ...
// clean up
var v = null;
var xmlHttp = null;
}
</script>
6 Posted by 4rn0 on 13 September 2005 | Permalink
@Mark
That is the way I tend to do it as well! For me 500 milliseconds (half a second) seems to be the ideal timeout, but then again I may not be the fastest typer....
7 Posted by Jeremy French on 13 September 2005 | Permalink
responseXML does not appear if Mozilla dosn't think the response is the correct mime type. What does it think that the mime type is in the error case, and can you read the response responseText?
8 Posted by ppk on 13 September 2005 | Permalink
A timeout of 500 before firing the request does indeed help. This timeout is of course reset if the user types a new character within the 500 ms.
Good idea. Thanks.
9 Posted by mray on 13 September 2005 | Permalink
The timeout is a great idea, but if you are in a situation where a timeout approach is not possible, you may also want to try a queueing mechanism to put all the requests in a queue and run them one at a time... Have to do this in Flash to keep getURL requests from stepping on each other, so it may work with XMLHTTP Requests as well.
10 Posted by Alex Collins on 13 September 2005 | Permalink
Queuing system is really useful if you're doing lots of asynchronous requests that deal with data that is related. As for the timeout, adjust it as you see fit. I also prevent any auto completion for inputs with less than three characters, which is ideal when you might return a hundred options if you don't get it right.
11 Posted by Analgesia on 13 September 2005 | Permalink
Great piece of research. I have been looking for the drawbacks of reusing xmlhttp.
does the same problem occure with responseText?
does it only occure directly after an aborted request?
12 Posted by Maciej Stachowiak on 15 September 2005 | Permalink
Why not just abort the old request, but then allocate a new one for the new request? That's much more likely to work reliably.
P.S. I wrote the XMLHttpRequest support in Safari/WebKit originally and I'm surprised that reusing the request works well, since I never really tested that. I did try to code it defensively against that case though.
13 Posted by Jonathan Perret on 15 September 2005 | Permalink
Regarding the documentation for the readyState property I'm surprised you didn't try to go to the horse's mouth (after all, Microsoft did invent XMLHttpRequest !) : http://msdn.microsoft.com/library/default.asp?url=/library/en-us/xmlsdk/html/0e6a34e4-f90c-489d-acff-cb44242fafc6.asp
14 Posted by Walter on 15 September 2005 | Permalink
I have this a problem like this.. I found some information here: http://www.ajaxblog.com/archives/2005/06/01/async-requests-over-an-unreliable-network
15 Posted by Garret Wilson on 19 September 2005 | Permalink
It gets worse. If Firefox has an ongoing XMLHTTPRequest when it posts a form (such as when a single input with an AJAX onchange event handler also causes a form post on Enter), simply accessing XMLHTTPRequest.status can throw an exception. Just to play it safe, I currently use:
try
{
status=xmlHTTP.status;
}
catch(e)
{
return;
}
16 Posted by Daniel Frechette on 21 September 2005 | Permalink
The same bug also exists in Firefox 1.0.6.
17 Posted by Andrew M Dietz on 22 September 2005 | Permalink
Hey, thanks for the tip! Just ran into that same problem in FireFox, and its nice to know about abort(). I store old requests in a global, then re-assign the global to the new one when a request is sent, so to get around this problem, I checked the old requests readyState against 0 and 4 (since anythign inbetween means it is working), and then aborted the old one. Gets overwritten two lines down, and my problem was solved. Kudos.
18 Posted by Ethan Alpert on 27 September 2005 | Permalink
What exactly does the abort() method send to the server? I'm trying to use it to cancel a long running perl cgi script under apache. I call abort yet the cgi continues to run. I tried catching signals but none are thrown.
19 Posted by Son Nguyen on 18 November 2005 | Permalink
Ethan Alpert,
I believe abort() doesn't send anything to the server, it'll abort the browser to send a new request. Thus, your CGI script which was executing would continue no matter what.
20 Posted by Tom Kagan on 19 November 2005 | Permalink
I think you want something more like this (hope this is legible):
var oHttp;
function newHttpReq() {
var NewH = null;
try {
NewH = new ActiveXObject("Msxml2.XMLHTTP");
}
catch(ee) {
try {
NewH = new ActiveXObject("Microsoft.XMLHTTP");
}
catch(eee) {
NewH = null;
}
}
if (!NewH && typeof XMLHttpRequest != "undefined") {
NewH = new XMLHttpRequest();
}
return NewH;
}
function Query(str, parm1) {
if (oHttp && oHttp.readyState != 0) {
oHttp.abort();
}
oHttp = newHttpReq();
if (oHttp) {
oHttp.open("GET","/backend.cgi?parm=" + parm1, true);
oHttp.onreadystatechange = function() {
if (oHttp.readyState == 4 && oHttp.responseText) {
// do something w/response
}
}
oHttp.send(null);
}
21 Posted by Theodore Hurst on 16 January 2006 | Permalink
To Garret Wilson and others:
The way to do form submission and an AJAX action such as a progress bar is to first submit the form and then call your method that starts the AJAX off in a setTimeout call. The setTimeout starts another thread to do the XMLHttpRequest and allows the thread that is doing the form submission to continue working and not be used to do a second post or get to the server, which is what I believe causes the exception to occur when one would look at the status propery of the XMLHttpRequest object.
Example JavaScript:
function submitForm() {
document.getElementById("theForm").submit();
var execString = "startAJAXProcess()";
setTimeout(execString, 200);
}
function startAJAXProcss() {
//XMLHttpRequest stuff
}
22 Posted by Tony on 22 February 2006 | Permalink
Maciej -
Your suggestion about aborting the old request and creating a new request worked just fine - although it's a bit awkward.
BTW, the bug still exists as of Firefox 1.5.0.1
23 Posted by Martin on 7 March 2006 | Permalink
I had a similar problem in FireFox with:
responseText and found that instead of using send(null) I used send(false) and everything worked okay for me, not sure if this will help with your problem though
24 Posted by Alex on 14 March 2006 | Permalink
if the XMLHttp request is dropped when using abort(), then why does the browser still wait for a reply from server before moving on?
For example: when clicking a button, I send an async POST request to a page and execute a code, keeping the server busy for 15 seconds. On document event onbeforeunload I use abort() on the request. What bothers me is that if I click the button then click refresh on the page, the browser will wait all 15 seconds before reloading the page even though the request was aborted.
Shouldn't abort stop and just ignore the request?
25 Posted by Siroki on 26 July 2006 | Permalink
I'm wondering if it can be solved by completely reconstructing the XMLHttpRequest object after abortion.