Errors and AJAX
In case you haven't heard by now, the hottest buzzword in the realm of web technology is AJAX (as coined in an Adaptive Path essay). The
crux of the AJAX framework is the XMLHttpRequest
JavaScript object which allows client-side developers to send and
receive XML documents over HTTP without interrupting the user, and
without hacking around with hidden frames. Now, some might shudder at
the notion of allowing client-side developers who might be more used
to validating forms and animating rollover images to suddenly be
responsible for traversing XML documents and negotiating HTTP headers,
but without risk, there is no reward. And just to soothe any lingering
trepidation, I will demonstrate how to use XMLHttpRequest
to not only add previously impossible or infeasible features, but also
to reduce errors and improve quality.
First, we need to set up a few ground
rules. The XMLHttpRequest object in particular, and XML
DOM in general, is widely supported in any recent browser (IE,
Mozilla, Safari, Opera) although, as usual, Microsoft has taken a
slightly different tack on implementation and requires some special
care. While our more progressive friends directly
implement XMLHttpRequest, IE requires that you
instantiate an ActiveXObject with the same properties. An excellent
overview and full feature list
is available
at the Apple Developer Connection site.
A basic example follows:
var req;
function postXML(xmlDoc) {
if (window.XMLHttpRequest) req = new XMLHttpRequest();
else if (window.ActiveXObject) req = new ActiveXObject("Microsoft.XMLHTTP");
else return; // fall on our sword
req.open(method, serverURI);
req.setRequestHeader('content-type', 'text/xml');
req.onreadystatechange = xmlPosted;
req.send(xmlDoc);
}
function xmlPosted() {
if (req.readyState != 4) return;
if (req.status == 200) {
var result = req.responseXML;
} else {
// fall on our sword
}
}
The potential uses for this powerful tool are vast, and exploration of the possibilities has just begun. But before anyone gets carried away with trying to create an XML circus on the web, I suggest we set up a safety net to keep any high-flyers from breaking their necks.
JavaScript has come a long way since its earlier versions, which were
crude, lacking in features, and just poorly implemented. Newer browsers
not only support the try/catch/finally keywords you will
recognize from C++ and Java, they also implement
an onerror event that can trap any error conditions that
arise during runtime. Usage is pretty straightforward:
function riskyBusiness() {
try {
riskyOperation1();
riskyOperation2();
} catch (e) {
// e is an object of type Error with
// at least two properties: name and message
} finally {
// clean up our mess
}
}
window.onerror = handleError; // safety net to trap all errors
function handleError(message, URI, line) {
// alert the user that this page may not respond properly
return true; // this will stop the default message
}
Now that we have the basics of XMLHttpRequest and
JavaScript error handling, let's look at an implementation that ties
the two together. You'd think that JavaScript errors would be easy to
spot given the prevalent "Yellow Triangle of Death", but I still see
them slip past the QA departments of several blue chip organizations'
public-facing web sites.
![]()
So, here I will present a method for trapping errors and logging
them back to the server in the hope that someone might be alerted to
fix it. First, let's consider our client. The client should provide a
class to be used as a singleton Logger object that can
transparently handle the gritty details.
First we create the constructor:
// singleton class constructor
function Logger() {
// fields
this.req;
// methods
this.errorToXML = errorToXML;
this.log = log;
}
Next, we define the method that will serialize
an Error object into XML. By default,
an Error object only has two
properties, name and message, but we will
also check for a third called location which may be
useful.
// map an error to an XML document
function errorToXML(err) {
var xml = '<?xml version="1.0"?>\n' +
'<error>\n' +
'<name>' + err.name + '</name>\n' +
'<message>' + err.message + '</message>\n';
if (err.location) xml += '<location>' + err.location +
'</location>';
xml += '</error>';
return xml;
}
Next is the log method. This is the meat and potatoes
of the script that really brings together the principles described above.
Notice that we are using the POST method for our
call. What I am essentially creating here is a bespoke web service
that is write-only and creates new records on each successful
request. Therefore, POST is the only appropriate option.
// log method of Logger class
function log(err) {
// feature sniff
if (window.XMLHttpRequest) this.req = new XMLHttpRequest();
else if (window.ActiveXObject) this.req =
new ActiveXObject("Microsoft.XMLHTTP");
else return; // throw up our hands in despair
// set the method and URI
this.req.open("POST", "/cgi-bin/AjaxLogger.cgi");
// set the request headers. REFERER will be the top-level
// URI which may differ from the location of the error if
// it occurs in an included .js file
this.req.setRequestHeader('REFERER', location.href);
this.req.setRequestHeader('content-type', 'text/xml');
// function to be called when the request is complete
this.req.onreadystatechange = errorLogged;
this.req.send(this.errorToXML(err));
// if the request doesn't complete in 10 seconds,
// something is up
this.timeout = window.setTimeout("abortLog();", 10000);
}
The last part of our class definition is to create an instance of
the Logger class. There should be only one instance of
this class.
// should only be one instance of the logger var logger = new Logger();
The last two functions are just there for housekeeping. If
something goes wrong while logging the error, there is not much we can
do except bother the user. Hopefully, it will never come to
this. These are not class methods since the events will not have
references to our object, but will refer to the logger
instance we just created.
// we tried, but if there is a connection error, it is hopeless
function abortLog() {
logger.req.abort();
alert("Attempt to log the error timed out.");
}
// called when the state of the request changes
function errorLogged() {
if (logger.req.readyState != 4) return;
window.clearTimeout(logger.timeout);
// request completed
if (logger.req.status >= 400)
alert('Attempt to log the error failed.');
}
All of the preceding code can be wrapped up into one .js file that can be included on any (or every) page in your site. Here is an example of how to include it and put it to good use:
<script type="text/javascript" src="Logger.js"></script>
<script type="text/javascript">
function trapError(msg, URI, ln) {
// wrap our unknown error condition in an object
var error = new Error(msg);
error.location = URI + ', line: ' + ln; // add custom property
logger.log(error);
warnUser();
return true; // stop the yellow triangle
}
window.onerror = trapError;
function foo() {
try {
riskyOperation();
} catch (err) {
// add custom property
err.location = location.href + ', function: foo()';
logger.log(err);
warnUser();
}
}
function warnUser() {
alert("An error has occurred while processing this page."+
"Our engineers have been alerted!");
// drastic action
location.href = '/path/to/error/page.html';
}
</script>
Now that we have seen how to integrate the logger into our HTML
pages, all that is left is to define some way of receiving and
translating the message. I have chosen a lowest common denominator
approach and built a CGI script in Perl that uses one of my favorite
modules, XML::Simple,
to parse the post data, and
CGI::Carp
to pipe the results directly to httpd error log, thus saving your system
administrators from having to monitor another log. This script also
includes some good examples of appropriate response codes for
different success and failure conditions.
use CGI;
use CGI::Carp qw(set_progname);
use XML::Simple;
my $request = CGI->new();
my $method = $request->request_method();
# method must be POST
if ($method eq 'POST') {
eval {
my $content_type = $request->content_type();
if ($content_type eq 'text/xml') {
print $request->header(-status =>
'415 Unsupported Media Type', -type => 'text/xml');
croak "Invalid content type: $content_type\n";
}
# when method is POST and the content type is neither
# URI encoded nor multipart form, the entire post
# is stuffed into one param: POSTDATA
my $error_xml = $request->param('POSTDATA');
my $ref = XML::Simple::XMLin($error_xml);
my ($name, $msg, $location) =
($ref->{'name'}, $ref->{'message'}, '');
$location = $ref->{'location'} if (defined($ref->{'location'}));
# this will change the name of the carper in the log
set_progname('Client-side error');
my $remote_host = $request->remote_host();
carp "name: [$name], msg: [$msg], location: [$location]";
};
if ($@) {
print $request->header(-status => '500 Internal server error',
-type => 'text/xml');
croak "Error while logging: $@";
} else {
# this response code indicates that the operation was a
# success, but the client should not expect any content
print $request->header(-status => '204 No content',
-type => 'text/xml');
}
} else {
print $request->header(-status => '405 Method not supported',
-type => 'text/xml');
croak "Unsupported method: $method";
}
And that's all there is to it! Now, the next time some slippery JavaScript gets into the system, you can expect your log monitors to start flashing red lights and your client-side developers to get calls in the middle of the night.
XML.com Copyright © 1998-2006 O'Reilly Media, Inc.