Tuesday, March 29, 2005

Make your Apache HTTP errors more Ajax-friendly

Applications that use remote scripting or Ajax-style communications can have problems when things go wrong at the server end. How does one detect a 404 or 500 error inside your hidden IFRAME, and how can an application be designed to handle such an error?

A client-side developer would probably suggest walking the DOM tree out to the IFRAME document and looking for the status text. But why not make it easier by having the server hand back HTTP errors to your application as JavaScript? This can be done with relative ease with Apache (I'm sure other servers can be configured similarly) with three basic steps:

  1. Create a custom error page
  2. Include JavaScript in the error page that interacts with the application (via the containing frame or IFRAME)
  3. Use Apache's ErrorDocument directive to tell the server to deliver the custom error page when an error occurs

Below is a sample error page written in PHP. Apache provides the basic variables needed to determine the status; here, we just pluck them from PHP's global variables and plug them into a JavaScript block. Once loaded, the JavaScript looks for an onServerError handler in the parent window (assuming the page request is made from a frame or IFRAME) and invokes it with the HTTP status information.

<?
  /* error.php */
  /* this line ensures the client gets the HTTP status */
  header("Status:" . $REDIRECT_STATUS);
?>
<!-- include DOCTYPE of choice here -->
<html>
<head>
  <title>Error <? print $REDIRECT_STATUS; ?></title>
  <script>
    var win = (window.parent) ? window.parent : window;
    if (win.onServerError) {
      win.onServerError(
        {
          status:'<? print $REDIRECT_STATUS; ?>',
          url:   '<? print $REDIRECT_URL; ?>',
        }
      );
    }
  </script>
</head>
<body>
  <h1>Error <? print $REDIRECT_STATUS; ?></h1>
  <p>
     This page should have invoked a 
     JavaScript onServerError handler 
     if one was provided.
  </p>
</body>
</html>

We'll save this file as error.php and place it in a directory named /rs-errors/. Next, we have to tell Apache to use our error page for selected HTTP error statuses. We can do this by adding ErrorDocument directives to an .htaccess file placed inside the directory at which our remote scripting requests are targeted. For example, if you're sending requests to http://appserver.example.com/api you'll want to put these in the .htaccess file for that directory:

ErrorDocument 404 /rs-errors/error.php
ErrorDocument 500 /rs-errors/error.php

Now, if a 404 or 500 error occurs inside of /api Apache will use our error.php as the error document served to the browser.

It should be relatively simple to implement this in a JavaScript application now:

<script>
  function onServerError(e) {
    switch (e.status) {
      case '500':
        // handle a 500 error here
        break;
      case '404':
        // handle a 404 error here
        break;
    }
  }
</script>

...

<iframe src="http://appserver.example.com/api/some-bogus-request-here">
</iframe>

When the IFRAME attempts to load the bogus URI, Apache will throw a 404 error and invoke error.php. The JavaScript in the head of our error page will attempt the onServerError handler defined in the application. The application can now decide how to best communicate the error state to the user without appearing broken.

Caveat: don't forget that IE will attempt to serve up its own custom 500 page from the local machine, so it might be a good idea to pad your error page with 512K 512 bytes (or more) of whitespace or commented text to convince IE that your error is the one to show.

If you prefer an "Ajax (The X Is For 'XML' Dammit!)" approach, you can easily write your custom error page to deliver HTTP statuses as an XML payload instead of JavaScript. I prefer JavaScript myself since I don't have to parse it, but XML might be preferred if you're sharing data across different apps on different platforms.

Reference:

Monday, March 28, 2005

Dynamic old-school IFRAMEs

Three years ago I lifted the following code for dynamically creating an IFRAME from Eric Costello's ADC article, Remote Scripting With IFRAME, and I've used some variation of it for years in my own development work. I modified it a bit since its original purpose was as part of a remote scripting engine; all I wanted was the IFRAME-generation code. Check out the crazy bit where Eric "fakes up" the IFRAME for IE5.

It's a wicked piece of code for the time it was written. It's one of those techniques that you can only learn about after several hours of doing — that is, messing around in a browser trying to get stuff to work. There's just no textbook for this kind of stuff.

Dunno if it works in Safari, Konqueror or Opera, although I'd love to hear if it does or doesn't.


/*
dynamic IFRAME code
original JS by Eric Costello (glish.com) for ADC
http://developer.apple.com/internet/webcontent/iframe.html
*/

var iframe, iframeDocument;
var iframeID = "MyHiddenIFrame";

if (document.createElement) {   
  try {
   
    var tempIFrame = document.createElement('iframe');
    tempIFrame.setAttribute('id',iframeID);
    tempIFrame.style.border = '0px';
    tempIFrame.style.width = '0px';
    tempIFrame.style.height = '0px';
    iframe = document.body.appendChild(tempIFrame);
      
    if (document.frames) {
      
      /* IE5 Mac only allows access to the document
      of the IFrame through frames collection */
        
      iframe = document.frames[iframeID];
    }
      
  } catch (ex) {
    
    /* This part is CRAZY! -- scottandrew */

    /* IE5 PC does not allow dynamic creation and 
    manipulation of an iframe object. Instead, we'll fake
    it up by creating our own objects. */
      
    var iframeHTML = '\<iframe id="' + iframeID + '"';
    iframeHTML += ' style="border:0px; width:0px; height:0px;';
    iframeHTML += '"><\/iframe>';
    document.body.innerHTML += iframeHTML;
    iframe = new Object();
    iframe.document = new Object();
    iframe.document.location = new Object();
    iframe.document.location.iframe = 
      document.getElementById(iframeID);
    iframe.document.location.replace = 
      function(location) {
        this.iframe.src = location;
      }
  }
  
  if (iframe.contentDocument) { // For NS6
    iframeDocument = iframe.contentDocument; 
  } else if (iframe.contentWindow) { // For IE5.5 and IE6
    iframeDocument = iframe.contentWindow.document;
  } else if (iframe.document) { // For IE5
    iframeDocument = iframe.document;
  } else { // damn!
    alert("Error: could not find IFRAME document");
  }
}

Saturday, March 26, 2005

Cool rounded corners with CSS and Javascript

Rounded corners in web design are neat, but usually require a handful of semantically meaningless positioned elements to achieve. Alessandro Fulciniti simply inserts those elements with some unobtrusive JavaScript. Check out the JS code and take a look at his custom getElementsBySelector() function, too.

Tuesday, March 22, 2005

A global event...in Firefox?

This is kind of nice:

function someHandler(a,x,y){
    alert(a.type);
}

<a href="#"
   onclick="someHandler(event,'x','y'); return false;">
   Do it</a>

Apparently, Firefox does support a global event object when passed as a parameter to an event handler, so the above code works in both Firefox and IE on Windows. You can't call window.event inside the function though, else Firefox will complain.

I've been doing the check for e for so long (see the previous post), like, waaaaay back in my DynAPI v1.0 days, that I hadn't noticed this technique was available.

Monday, March 21, 2005

Really basic event confusion

This is another no-brainer, but I still see this all the time:

function someEventHandler(e) {
    alert(e.type);
}

Works in Firefox and other Moz browsers. IE pops an error. Why? Because in IE browsers, the event (e) object is a property of the window, not a local argument passed to the handler. So e is undefined, in IE's view. To get around this, try:

function someEventHandler(e) {
    if (!e) e = window.event;
    alert(e.type);
}

The amusing thing about having to post this is I hear this question a lot from developers who are used to coding only for Firefox. How times change, eh?

Friday, March 18, 2005

Cross-domain security woes

You're developing an Ajax-based application. You have an application server at example.com which serves up all your JavaScript, HTML and CSS, and a data server at xml.example.com which delivers all the XML data to the application via a hidden IFRAME.

You know that cross-domain security will prevent any JavaScript from accessing the data in the IFRAME. so, you configure the data server to set the security domain of the IFRAME to "example.com" — the common suffix between the two domains — with a small piece of JavaScript:

<script type="text/javascript">
  document.domain="example.com";
</script>

Having done this, you test your application and get a "permission denied" error. What happened?

Depending on your browser, it may not be enough to only set the security domain of the IFRAME. You must set all of the frames and windows to the same domain, too. This is true even if the domain name you're trying to set already matches the domain of the server that's currently serving the page. For example, if you have two frames with pages served from example.com and you use JavaScript to set the security domain of one frame to "example.com" the frames will be unable to communicate.

Older browsers might let you get away with this.

About this site

The Strange Zen Of JavaScript is an offshoot of and successor to Delimiter, a weblog about web application design focused on JavaScript, CSS, Flash and other client-side technologies. Unlike Delimiter, this weblog will try to keep the focus on JavaScript quirks, caveats odd hacks, curiosities and collected wisdom, and only delve into topics like CSS where they intersect. I'm also going to try and refrain from commenting on every cool JS-based app released unless there's a useful technique or exploit to be learned from reverse-engineering.

I got the idea for SZOJ while perusing the Delimiter archives and rediscovering a series of numbered posts dealing with strange little JS gotchas I'd uncovered. I decided to consolidate these posts in a new weblog, and I'll probably end up plundering some old content from my formerly-all-scripting site scottandrew.com and seeing how it's held up.

Some of these posts might contain stuff that advanced developers will find forehead-slappingly obvious, but everyone starts somewhere. My hope is that web app developers who run into trouble might Google this site and find an answer, or at least something interesting.

Feel free to send me any JS weirdness you discover, and I'll be happy to post it. Be sure to include a name and a URL so I can credit you. Here's the address.

XMLHTTP and readyState

Given how the world of web design is currently enamored with Ajax — a.k.a. Asynchronous JavaScript + XML, a.k.a. remote scripting with XML messages, a.k.a. a technology that back in 2000 was considered way too rawk star and subversive until Google started playing with it recently — I thought I'd unearth this oldie-but-goodie from 2002. Enjoy.

Microsoft's XMLHTTP object is an ActiveX component that lets you perform HTTP requests. The object has four different states that it cycles through when performing a request. One of those states is called "interactive" and is decribed as such (from the MSDN documentation):

(3) INTERACTIVE
Some data has been received. You can call responseBody and responseText to get the current partial results.

...meaning that at that point you should be able to examine the contents of the response so far. Let's try this, shall we?

var xmlHTTP = new ActiveXObject("Microsoft.XMLHTTP");
xmlHTTP.onreadystatechange = handleStateChange;

function handleStateChange()
{        
    if (xmlHTTP.readyState == 3)
    {
        alert(xmlHTTP.responseText);
    }
}

xmlHTTP.open("POST","/some/uri",true);
xmlHTTP.send();

Execution of this code produces an error in the handler:

The data necessary to complete this operation is not yet available.

...which troubles me greatly. Waiting until readyState is 4 (complete) works perfectly, but there's an added complication: the URI /some/uri establishes a persistent connection, which means that unless the connection is terminated by the script or the server, readyState will always be 3 (interactive). According to the docs, I should be able grab the incomplete response from the server at this time, but apparently not. Possible reasons:

  • the MSDN documentation is a big fat lie incorrect
  • the XMLHTTP object has a huge buffer to fill before it will make the response available
  • the object needs to see some sort of delimiter, like a null byte, as a signal to make the response text available
  • I'm horribly misunderstanding the whole thing

UPDATE 3/4/2004: many. many people have written me to ask if I have found a workaround since I first posted this. I'm sorry to say that I have not.

Inline script documentation with comment hacking

I'm still in love with Tantek's extremely clever way to include HTML documentation for JavaScript code inside the same document using comment hacks.

The particular (and peculiar) sequence of SGML comments <!-- --> and C-style comments /* */ allow the document to be parsed as either HTML or JavaScript depending on the way it's imported into the document. Load the script with a SCRIPT element and it's interpreted as JavaScript, but load the script directly into the browser and it's displayed as glorious, technicolor HTML. One caveat: you must leave off the .js file extension so that IE will interpret the file as text/html when you drop it into a browser.