Cross Domain ExtJS File Uploading

Uploading forms is a basic operation for most applications along with other asynchronous requests, many which may go cross domain.  While we now have CORS support for most operations, file uploading in ExtJS is behind.  Ext.form.Basic.hasUpload describes the file uploading process

Returns true if the form contains a file upload field. This is used to determine the method for submitting the form: File uploads are not performed using normal ‘Ajax’ techniques, that is they are not performed using XMLHttpRequests. Instead a hidden <form> element containing all the fields is created temporarily and submitted with its target set to refer to a dynamically generated, hidden <iframe> which is inserted into the document but removed after the return data has been gathered.

This is troublesome if you have a nice API and are planning on serving your assets statically.  To get file uploading to work as intended you now have to have the server handle the entirety of the of the request from that particular domain.  So there goes using api.mycoolsite.com to handle requests to www.mycoolsite.com.

So to avoid this I propose the following potential solution.  This will allow you to keep your api.mycoolsite.com, avoid issues with CORS, and get your files saved the way you want.

ExtJS File Uploading Solution

So to make this solution work we’ll create a web service to post the data too but manipulate the output from the request. This article won’t go into making a multipart/form-data service but we’ll look at the output we need to manipulate. To make this work the multipart/form-data post should return a webpage. The webpage should contain a script as follows:

(function() {
  window.parent.postMessage({
    // data goes here
  }, '*');
})();

The idea here is to send a message from the iframe POST created by Sencha through the standard mechanism of the postMessage JS function. Luckily this is supported by all browsers so it should hopefully just work out of box. The only downside here is that you have to also post a message if it fails so you have to swallow any errors and manipulate them to fit inside of this postMessage in the multipart/form-data POST response.

The Sencha JS side of it the work is pretty straightforward then

var listener = function(event) {
    if(event.data.originalName === Ext.ComponentQuery.query('#attachments')[0].getValue().split('\\').pop()) {
        window.removeEventListener('message', listener);
        listener = null;
    }
}
window.addEventListener('message', listener);
form.submit({
  url: 'https://myservice/fileuploader/upload',
  waitMsg: 'Uploading...',
  failure: function(form, action) {
      setTimeout(function() {
          if(listener) {
              Ext.Msg.alert('Failure', 'Your document has NOT been uploaded.');
              window.removeEventListener('message', listener);
              listener = null;
          }
      }, 1000);
  }
});

This adds a listener to capture the event that gets created by the postMessage() in the iframe. The setTimeout() may need the speed adjusted slightly depending on your networking but this forces the listener exists check to the end of the chain guaranteeing it is the last operation performed. So if the postMessage() does not remove the listener, assume the file upload had a hard failure – most likely a 400 error. 500 errors should be processed in the postMessage and successfully clear the listener.