Tuesday, May 06, 2008

AJAX File Upload: The Cake is a Lie

For a long time I have been smitten with the idea of AJAX. By now everyone has experienced AJAX, even if they don't know it yet. AJAX powers web 2.0 sites like Flickr and Gmail. Allowing the user to interact with a website without a page-refresh is a strangely liberating technology... finally my applications have state! But the true holy grail of AJAX lies with the mysterious mechanism of file uploading. No doubt you've done this before, in a non-ajax fashion. While filling out some innocuous HTML form you are presented by a seemingly innocent file selection dialog box, perhaps selecting the latest photo of you kitty, to send along with the other information. This basic file uploading capability is made possible by creating a special HTML form, like so:
<form action='upload.cgi' method='post' enctype='multipart/form-data'>
...HTML form fields go here...
<input type='file' name='my_picture'>
...maybe some more HTML form fields go here...
<input type='submit' value='Down the Tubes!'>
</form>
That enctype business there tells your browser to send a special sort of HTTP request that can contain binary data. Generally requests just send text, but by enabling binary data transmission, we can send photos, mp3s, pdfs, anything within the size limit of the protocol. Trouble is, ajax requests are built such that you cannot change the enctype to multipart/form-data! Even with the cross-browser prowess of Prototype (my preferred javascript framework), there is just no way to change the nature of the HTTP request. It's either text or bust.

So, how do internet giants like Flickr and Facebook do it? What is the secret ingredient? A little googling reveals the answer as satisfactory, yet unsatisfying. Allow me to explain. To start, we need to redefine our objectives... since we can't "use AJAX to upload a file" our objective needs to be "make it appear like we are using AJAX to upload a file." When we say, "use AJAX" what we really mean is communicate with the server without a page reload. But we must remember the earlier lesson, you can only upload a file use a multipart/form-data form. Put another way, we have to call submit() on that form... there is no other path to the promised land.

HTML Forms are a tricky thing. Left to their own devices, when you call submit(), the entire page reloads. So that's out. But, we can set a target for the form, such that calling submit() causes the form to load in the target window. Setting target='_new' will create an entirely new window where the form will be processed. This is sort of cool, in that the underlying window remains unchanged. But we certainly don't want new windows popup up all the time. Yuck.

We could set the target to an embedded iframe in the main window itself. This is a lot closer, because there is no messy popup business. But now you've got this iframe reloading, which isn't exactly the seamless experience we are shooting for. The final piece to our puzzle then is to make the iframe hidden with style='display: none;' attribute.

So, now are form from above looks like this.
<form action='upload.cgi' method='post' target='empty_iframe' 
enctype='multipart/form-data'>
...HTML form fields go here...
<input type='file' name='my_picture'>
...maybe some more HTML form fields go here...
<input type='submit' value='Down the Tubes!'>
</form>
<iframe src='about:blank' name='empty_iframe'
style='display: none;'></iframe>
Now, when you hit the submit button the form sends the data, including the file, off to the server and the response comes back to the invisible iframe. To the user, nothing seems to have changed. You can add a little pre-process magic with javascript, like hiding the form, but what if you want to do post-process magic? With a traditional AJAX request you could get an XML payload back, or javascript if you use a framework like Prototype. Turns out you can do something similar with the iframe trick. You can call methods on the parent window from within the iframe by sending javascript inside of a <script> tag.

Your output to the iframe will looking something like this:
<script type='text/javascript'>
window.top.window.function();
</script>
You can call as many functions from within the script tags as you like, just remember that the iframe has no sense of the variables available from within the parent window, so that can complicate things. But a little forth-thought can go a long way to making the magic happen. You can also do some cool things like insert and remove the iframe on the fly so that it's only there during the form processing bit.

Now that you know how it works, it should be obvious how this is all a lie... a horrible, horrible lie. There isn't anything the least bit AJAX-Y about this. In fact, if you accept this as a valid method of asynchronous server communication, then you can pretty much never use the XMLHttpRequest object ever again... just communicate via hidden iframes! I realize that file uploading is a serious security concern (we don't want malicious coders to be able to upload files from your harddrive without your knowledge), and I know that AJAX presents it's own security concerns... but there has got to be a better way. I hope that future revisions to the XMLHttpRequest object provide a way to send multipart/form-data responses so we can ditch this awful, messy hack.

4 comments:

Anonymous said...

Clap! Clap! Clap!
Very Nice!

Matt York said...

YES! I stumbled across the exact same thing, but I had a different take on the whole thing: what the hell are we doing shifting our precious applications onto the intertubes with the current web technologies? They suck.

Anonymous said...

Such a great exposition of the problem and the solution... and yet completely missing the point: AJAX will never do file post-ing because then it would have to access the underlying file system of your computer. Maybe browsers will at some point find it so vital to do so that they'll add some popup security alert informing you that javascript wants to access your local harddrive, but for myself, I prefer being forced to work with iframes and the like (and it's not like it's all that hard, as all can see from your description here), for the sake of my own personal security when browsing the web. There are enough dangers already.

Anonymous said...

Great simple example thanks