Errors and AJAX

Friday 21st of July 2017 10:53:00 PM 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.

XMLHttpRequest and XML DOM for JavaScript Basics

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 {
}
}

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 Error Handling Basics

JavaScript has come a long way since its earlier versions, which were crude, lacking in features, and just poorly implemented. Newer browsers

User agents are not, according tothe CSS1 specification,required to fully support negative margins, using the phrase,"A negative value is allowed, but there may beimplementation-specific limits." In the world of web browsers,though Navigator 4.x, Explorer 4.x/5.x, and Opera 3.x do permitnegative margins:

Negative margins have an impact on vertical formatting, affecting how 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) {
} finally {
}
}
window.onerror = handleError; // safety net to trap all errors
function handleError(message, URI, line) {
return true; // this will stop the default message
}

A Practical Example: Persisting Client-side Errors to your Server

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.

Figure 1

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:

function Logger() {
this.req;
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.

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.

function log(err) {
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
this.req.open("POST", "/cgi-bin/AjaxLogger.cgi");
this.req.setRequestHeader('REFERER', location.href);
this.req.setRequestHeader('content-type', 'text/xml');
this.req.onreadystatechange = errorLogged;
this.req.send(this.errorToXML(err));
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.

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.

function abortLog() {
logger.req.abort();
alert("Attempt to log the error timed out.");
}
function errorLogged() {
if (logger.req.readyState != 4) return;
window.clearTimeout(logger.timeout);
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) {
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) {
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!");
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.

gopher://  = Gopher information search
  • mailto:  = compose and send e-mail (note: no slashes)
  • news:  = read/send to Usenet newsgroup
  • file:///  = local file access (note: 3 slashes)
  • Web servers typically have 3- or 4-part names; the last two parts comprise the registered domain name, e.g., udel.edu.  Some web text.

    11.2.6. The Incredible Shrinking Text!

    Here'sa fun thing to do: make your document text so small that itcan't be read by the human eye. You can do this using

    Figure 4-37

    Figure 4-37. Middle alignment

    In practice, since most user agents treat 1ex asone-half em, middle will cause the verticalmidpoint of an element to be aligned with a point one-quarter emabove the parent's baseline. Figure 4-38 showsthis in more detail.

    Figure 4-38

    Figure 4-38. Precise detail of middle alignment

    This is pretty close to simulating <IMGALIGN="middle">, as you can see, andtop, right,bottom, and left, plus anymargins, padding, and borders set for the element. Absolutelypositioned elements can have margins, but these margins do notcollapse.

    fixed

    The element's box is positioned as though it were set toCreate your own Java object model that imports information from the XML document by using either SAX or DOM. This kind of object model only uses SAX or DOM to initialize itself with the information contained in the XML document(s). Once the parsing and initialization of your object model is completed, DOM or SAX isn't used anymore. You can use your own object model to accessed or modify your information without using SAX or DOM anymore. So you manipulate your information using your own objects, and rely on the SAX or DOM APIs to import the information from your ApplicationML file into memory (as a bunch of Java objects). You can think of this object model as an in-memory instance of the information that came was "serialized" in your XML document(s). Changes made to this object model are made persistent automatically, you have to deal with persistence issues (ie, write code to save your object model to a persistence layer as XML).

  • Create your own Java object model (adapter) that uses DOM to manipulate the information in your document object tree (that is created by the parser). This is slightly different from the 2nd option, because you are still using the DOM API to manipulate the document information as a tree of nodes, but you are just wrapping an application specific API around the DOM objects, so its easier for you to write the code. So your object model is an adapter on top of DOM (ie, it uses the adapter pattern). This application specific API uses DOM and actually accesses or modifies information by going to the tree of nodes. Changes made to the object model still have to be made persistence (if you want to save any changes). You are in essence creating a thin layer on top of the tree of nodes that the parser creates, where the tree of nodes is accessed or modified eventually depending on what methods you invoke on your object model.
  • Depending on which of the three options you use to access information using your Java classes, this information must at some point be saved back to a file (probably to the one from which it was read). When the user of your application invokes a File->Save action, the information in the application must be written out to an ApplicationML file. Now this information is stored in memory, either as a (DOM) tree of nodes, or in your own proprietary object model. Also note that most DOM XML parsers can generate XML code from DOM document objects (but its quite trivial to turn a tree of nodes into XML by writing the code to do it yourself). There are 2 basic ways to get this information back into an ApplicationML file: