Cross-Domain, Cross-Browser AJAX Requests

| 8 minutes | Comments

This article describes how to make cross-browser requests, in all browsers (including IExplorer 6), using web standards along with fallbacks and without using a proxy or JSONP (which is limited and awkward) -- as long as you control the destination server, or if the destination server allows.

I’m explaining this file: crossdomain-ajax.js

Updates #

Oct 27, 2011 #

Added restrictions of usage and removed functionality that doesn’t work on IExplorer. So in case this doesn’t work for you, please see this page: Troubleshooting

In Modern Browsers - Meet Cross-Origin Resource Sharing #

Or CORS for short, or HTTP Access Control, available in recent browsers, allows you to make cross-domain HTTP requests; the only requirement being that you must have control over the server-side implementation of the domain targeted in your XMLHttpRequest calls.

This little piece of technology is available since Firefox 3.5 / IExplorer 8 and yet when searching for answers on websites like StackOverflow, it rarely comes up.

For the purposes of this tutorial, we’ll assume we want to make a request from website http://source.com to http://destination.org, and that you control the implementation to both.

var xhr = new XMLHttpRequest();
// NOPE, it doesn't work, yet
xhr.open("POST", "http://destination.org", true);

Response of destination.org #

It’s pretty simple really, all you need to do is to return these headers in your response:

Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://source.com
Access-Control-Allow-Headers: Content-Type, *

You can find a description of them on Mozilla Doc Center, but the most important one is Access-Control-Allow-Origin, which indicates the Origin(s) allowed to make such a request.

Note: these options allow for wildcards (like you can say that you allow for any Origin by putting a “*” in that header), but it is better to be explicit about what’s allowed, otherwise your request won’t work very well cross-browser.

(New) Note: In regards to Access-Control-Allow-Origin, IExplorer DOES NOT support wildcards. See Troubleshooting for details.

Client-side Implementation of Ajax Request for CORS #

On browsers where XMLHttpRequest is valid, support for CORS can be validated by checking for the availability of the withCredentials property.

So we’ve got a tiny issue: IExplorer's implementation is different than that of Firefox’s or the rest of the browsers (naturally). Instead of using the same XMLHttpRequest object, IExplorer 8 adds an XDomainRequest object.

So to initialize an async request, that will work on IExplorer 8, Firefox, Chrome and the other browsers supporting it:

try {
    var xhr = new XMLHttpRequest();
} catch(e) {}

if (xhr && "withCredentials" in xhr){
  xhr.open(type, url, true);
} else if (typeof XDomainRequest != "undefined"){
  xhr = new XDomainRequest();
  xhr.open(type, url);
} else {
  xhr = null;
}

But we aren’t done yet, the callbacks used by these request objects have different behavior on IExplorer. So let’s say we’ve got 2 callbacks that we want to register, one for success, one for errors, having the following signatures (same as jQuery):

function success(responseText, XHRobj) { ... }
function error(XHRobj) { ... }

To have correct behavior cross-browser:

//
// combines the success/error handlers into one
// higher-order function (getting a little fancy for code-reuse)
//
var handle_load = function (event_type) {
  return function (XHRobj) {
    //
    // stupid IExplorer won't receive any param on callbacks!!!
    // thus the object used is the initial `xhr` object
    // (bound to this function because it's a closure)
    //
    var XHRobj = is_iexplorer() ? xhr : XHRobj;

    //
    // IExplorer also skips on readyState
    // Also, it's success/error based on the `event_type` used at the call-site
    //
    if (event_type == 'load' && (is_iexplorer() || XHRobj.readyState == 4) && success)
      success(XHRobj.responseText, XHRobj);
    else if (error)
      error(XHRobj);
  }
};

try {
  // IExplorer throws an exception on this one
  //
  // Setting this to `true` is specifying to make the request with Cookies attached.
  // BUT -- it's pretty useless, as IExplorer doesn't support sending Cookies.
  //
  // Also, trying to set cookies from the response is not really possible directly
  // (workarounds are available though -- you can return anything in the response's
  //  body and use local javascript for persistence/propagation on next request)
  //
  xhr.withCredentials = false;
} catch(e) {};

//
// `onload` + `onerror` are actually new additions to these browsers.
//
// IExplorer doesn't actually push params on calling these callbacks.
// For every other browser, the XHRobj we want is in `e.target`,
// where `e` is an event object.
//
xhr.onload  = function (e) {
  handle_load('load')(is_iexplorer() ? e : e.target)
};
xhr.onerror = function (e) {
  handle_load('error')(is_iexplorer() ? e : e.target)
};
xhr.send(data);

Also of notice, here’s how to check if the browser is IExplorer:

function is_iexplorer() {
  return navigator.userAgent.indexOf('MSIE') !=-1
}

Well, that’s it, unless you want to support the rest of desktop browsers in use.

Fallback for Older Browsers #

Opera 10 doesn’t have this feature, neither do IExplorer < 8, Firefox < 3.5 – and I don’t really know when Chrome/Safari added it. Fortunately there’s a workaround – Flash can do whatever you want and runs the same on ~90% of desktop browsers out there, AND it can interact with Javascript.

Not to reinvent the wheel, here’s a cool plugin: flensed.flXHR.

Why bother with CORS? #

  • Flash is not available on the iPhone
  • Flash loads slower than Javascript
  • Flash SWF files come with a lot of junk that your browser has to download
  • The whole experience using flXHR will be visibly slower than with CORS

flXHR Usage #

//
// Does a request using flXHR (the JS-Flash alternative
// implementation for XMLHttpRequest)
//
function _ajax_with_flxhr(options) {
  var url = options['url'];
  var type = options['type'] || 'GET';
  var success = options['success'];
  var error = options['error'];
  var data = options['data'];

  //
  // handles callbacks, just as above
  //
  function handle_load(XHRobj) {
    if (XHRobj.readyState == 4) {
      if (XHRobj.status == 200 && success)
        success(XHRobj.responseText, XHRobj);
      else
        error(XHRobj);
    }
  }

  var flproxy = new flensed.flXHR({
    autoUpdatePlayer: false,
    instanceId: "myproxy1",
    xmlResponseText: false,
    onreadystatechange: handle_load
  });

  flproxy.open(type, url, true);
  flproxy.send(data);
}

We are NOT done. The destination server also needs a file called crossdomain.xml, which represents a Crossdomain Policy File Spec. As a requirement, this file has to be placed in the domain’s root, i.e. http://destination.org/crossdomain.xml

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <!-- wildcard means 'allow all' -->
  <allow-access-from domain="*" />
  <allow-http-request-headers-from domain="*" headers="*" />
</cross-domain-policy>

Not Loading the Junk when Not Needed #

Javascript is asynchronous and we should take advantage of that by not loading flensed.flXHR, unless needed and at the last moment too (no need to load it until we want to make a request).

We need a method for asynchronously loading a Javascript file and executing a callback onload. And since we may be executing this function multiple times at once, we need to take care of race-conditions. First things first:

// keeps count of files already included
var FILES_INCLUDED = {};

// keeps count of files in the processes of getting loaded
// for avoiding race conditions
var FILES_LOADING = {};

// stacks of registered callbacks, that will get executed once
// a file loads -- this to deal with multiple file inclusions at once,
// and not ignoring anything
var REGISTERED_CALLBACKS = {};

function register_callback(file, callback) {
  if (!REGISTERED_CALLBACKS[file])
    REGISTERED_CALLBACKS[file] = new Array();
  REGISTERED_CALLBACKS[file].push(callback);
}

function execute_callbacks(file) {
  while (REGISTERED_CALLBACKS[file].length > 0) {
    var callback = REGISTERED_CALLBACKS[file].pop();
    if (callback) callback();
  }
}

To asynchronously load a Javascript file, with onload callback, behold:

//
// Loads a Javascript file asynchronously, executing a `callback`
// if/when file gets loaded.
//
// Returns `true` if callback got executed immediately, `false` otherwise.
//
function async_load_javascript(file, callback) {
  // stores callback in the stack
  register_callback(file, callback);

  // dealing with race conditions

  if (FILES_INCLUDED[file]) {
    execute_callbacks(file);
    return true;
  }
  if (FILES_LOADING[file])
    return false;

  FILES_LOADING[file] = true;

  // dynamically adds a <script> tag to the document
  var html_doc = document.getElementsByTagName('head')[0];
  js = document.createElement('script');
  js.setAttribute('type', 'text/javascript');
  js.setAttribute('src', file);
  html_doc.appendChild(js);

  // onload, then go through the stack of callbacks,
  // and execute all of them
  js.onreadystatechange = function () {
    if (js.readyState == 'complete' || js.readyState == 'loaded') {
      if (!FILES_INCLUDED[file]) {
        FILES_INCLUDED[file] = true;
        execute_callbacks(file);
      }
    }
  };

  // same as above, same shit for dealing with incompatibilities
  js.onload = function () {
    if (!FILES_INCLUDED[file]) {
      FILES_INCLUDED[file] = true;
      execute_callbacks(file);
    }
  };

  return false;
}

Almost there #

To bind it all together we need to plug this into our main logic. So if browser does not support CORS, it fallbacks to this implementation.

// to recapitulate
if (xhr && "withCredentials" in xhr) {
  xhr.open(type, url, true);
} else if (typeof XDomainRequest != "undefined") {
  xhr = new XDomainRequest();
  xhr.open(type, url);
} else {
  xhr = null;
}

// NOT SUPPORTED, then fallback
if (!xhr) {
  async_load_javascript(CROSSDOMAINJS_PATH + "flXHR/flXHR.js", function () {
    _ajax_with_flxhr(options);
  });
}

To see the final code, go here: crossdomain-ajax.js.

| Written by
Tags: JavaScript | Browser | Web