AJAX File Uploads with the iFrame Method

09 May 2011

AJAX file uploads, how do they work?! Well, they kinda don't.

Browsers don't allow file uploads via XMLHttpRequest (aka XHR) for security reasons. If we try to submit a form remotely via XHR, it will work, except with the file field stripped out of the request parameters. This sort of partial, silent failure can lead to unicorn black-eyes. (I punch unicorns in the face when I'm frustrated.)

This is how jQuery's standard AJAX functions work, including .ajax().

In turn, the Rails 3 jQuery UJS driver uses jQuery's standard AJAX functions internally. To prevent this sort of silent failure in Rails 3, we added the ajax:aborted:file event to abort the remote form submission if any non-blank file inputs are detected.

See New ajax:aborted Rails jQuery UJS Hooks.

Workarounds

So, there are a couple workarounds that give the impression of AJAX file uploads (and thus the same workflow and UI from the user's perspective), without actually submitting the file via XHR.

One method is to use Flash. Since Flash makes connects to the server outside the scope of the browser's connection, it essentially plays by it's own rules.

Another, more popular method (since it works in browsers without requiring Flash to be installed) is known as the iFrame method. And here's how it works.

The iFrame method

From here on, I'll be referring specifically to the ajaxSubmit() method built into the jQuery form.js plugin, but the concepts are generally the same for any library that uses the iFrame method.

Assuming we have a form with a file-type input field, the iFrame method of uploading files can be summarized in the following steps:

  1. Hijack the forms submit event to execute our custom iFrame-method function (e.g. .ajaxSubmit() from the form.js plugin).
  2. Using JavaScript, create an iFrame element and insert it into the current page (and make the iFrame tiny and invisible to the user).
  3.  
    Thanks to spinn over on Reddit for catching this. I've updated this step to illustrate the more correct way.
    Thanks to malsup in the comments for keeping me honest here and suggesting a further clarification.
    Change the target attribute of the form, such that the results of the form submission are rendered in the new iFrame instead of the current window.
  4. Submit the form to the iFrame normal-style (non-AJAX).
  5. Allow the iFrame to navigate to the response page (since it was submitted normal-style). Note that because this happens in the iframe, the parent window does not go to a new page or get redirected, only the hidden iFrame.
  6. Copy the response content from the iFrame back into the parent window.
  7. Delete the iFrame, reset the form's target attribute to its original value, and inform the .ajaxSubmit() (from form.js) function's callback hooks that the submission is complete.

It's that easy! Er, not quite...

Complications

The above steps have a couple implications to further complicate things...

Response headers and status code

Since the submission takes place normal-style in an iframe, the parent window cannot inspect the response headers or status code (from step 5 above), due to browser security restrictions.

So, .ajaxSubmit() in the parent window must assume, once the request is completed, that it was successful. There are ways around this, but as of this writing, they have not yet been pulled into the form.js plugin.

Response JavaScript not automatically executed

Furthermore, if a JS response was returned, it won't get executed in the parent window, because it was returned to the iframe. We would thus need to manually check if the response was JS code (instead of HTML markup or some other format), and execute it ourselves in step 7 above.

Form.js has a workaround which allows you can return some javascript code inside a textarea element as the response content. So then, if the iframe response content contains a textarea with some text value once the iframe form is submitted, it will copy that textarea content back to the parent window and execute it as javascript.

AJAX file uploads in Rails 3

Now that I've set the stage, in my next article, I'll show how we can seamlessly add AJAX file upload capabilities to Rails 3 by extending jquery-ujs.

Future-proof

Thanks to Gábor in the comments below for bringing this up and prompting me to add this section.

Of course, we must ask, why are the browsers making it so hard for us to do something so simple? After all, is it really making the user's interaction any more secure if there is such a workaround, as described here, which is as useful as it is popular? The answer is, no not really. And that is why the newest browsers have started to implement an actual AJAX file upload API.

As mentioned in the comments, we can see that a few of the latest browsers are already supporting this. For an idea of how to use this API, check out Mozilla's documentation for using it with Firefox 4.

About the author:

Steve Schwartz // Owner of Alfa Jango Web-based Software, creator of RateMyStudentRental & LeadNuke, engineer, hacker, rubyist, guitarist, aspiring racecar driverist.



Comments are loading...