Cross-Domain Requests and Prototype

  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.
  • : Function split() is deprecated in /home/kourge/kourge.net/modules/filter/filter.module on line 895.

Ever since 3.5, cross-domain HTTP requests have been supported in Firefox. Getting this to work may seem easier and saner than piggy-backing Flash ("simply request a URL of different origin in your XMLHttpRequest!"), but unless you use jQuery, you'll realize that for some reason, cross-domain requests mysteriously fail in your JavaScript framework despite the fact that the server is correctly responding with an appropriate Access-Control-Allow-Origin header.

To understand why this happens, consider the types of requests outlined in the MDC document on HTTP access control: excluding scenarios wherein authentication is needed, requests are divided into simple requests and preflighted requests. Simply put, preflighted requests require your server-side script to support the HTTP OPTIONS method, while simple requests merely require the script to respond with the appropriate header(s). The point of failure, if you start watching what actually happens over the wire, is that requests that should've stayed simple are being preflighted. The MDC document states that:

A simple cross-site request is one that:

[...]

  • Does not set custom headers with the HTTP Request (such as X-Modified, etc.)

Most major Ajax frameworks like to set custom HTTP headers on the Ajax requests you instantiate; the most popular header is X-Requested-With: XMLHttpRequest. Consequently your request is promoted to a preflighted one and fails. The fix is to prevent your JavaScript framework from setting these custom headers if your request is a cross-domain one. jQuery already cleverly avoids unintentionally preflighting requests by not setting custom headers if your URL is considered to be remote. You'd have to manually prevent this if you're using other frameworks.

I'll be demonstrating how to fix this in Prototype. Why Prototype? Because Dojo lets you override the setting of a header by specifying it as a blank string, and Mootools separates the act of instantiating a request and sending it into two methods. Prototype, on the other hand, fires the request right after you instantiate a new Ajax.Request.

Typically, this is how you fire off an Ajax request in Prototype:

new Ajax.Request("http://summit.mozilla.org/callback.php", {
  method: "get", parameters: {last: Math.round(new Date().valueOf() / 1000)},
  onSuccess: function(transport) {
    // transport.responseText
  }
});

Since the custom headers Prototype sets are hard-coded, the quick, temporary fix is to replace the setRequestHeader method of your XMLHttpRequest after creating but before sending by using the onCreate callback:

new Ajax.Request("http://summit.mozilla.org/callback.php", {
  method: "get", parameters: {last: Math.round(new Date().valueOf() / 1000)},
  onCreate: function(request) {
    request.transport.setRequestHeader = Prototype.emptyFunction;
  },
  onSuccess: function(transport) {
    // transport.responseText
  }
});

This all seems very onerous since you need to remember to tack on this onCreate callback every time you fire off a request. Prototype fortunately provides Ajax.Responders, which lets you register callbacks for every Ajax request created through Prototype. Here's the refactored version:

Ajax.Responders.register({
  onCreate: function(response) {
    response.transport.setRequestHeader = Prototype.emptyFunction;
  }
});

new Ajax.Request("http://summit.mozilla.org/callback.php", {
  method: "get", parameters: {last: Math.round(new Date().valueOf() / 1000)},
  onSuccess: function(transport) {
    // transport.responseText
  }
});

Now another problem arises: you can't set custom headers at all, even in scenarios where you legitimately need to! So exactly which headers aren't considered custom enough to trigger preflighting? According to the W3C spec on Cross-Origin Resource Sharing:

A header is said to be a simple header if the header field name is an ASCII case-insensitive match for Accept, Accept-Language, or Content-Language, or if it is an ASCII case-insensitive match for Content-Type and the header field value media type (excluding parameters) is an ASCII case-insensitive match for application/x-www-form-urlencoded, multipart/form-data, or text/plain.

Instead of replacing setRequestHeader with a function that does nothing, we now wrap around it:

Ajax.Responders.register({
  onCreate: function(response) {
    var t = response.transport;
    t.setRequestHeader = t.setRequestHeader.wrap(function(original, k, v) {
      if (/^(accept|accept-language|content-language)$/i.test(k))
        return original(k, v);
      if (/^content-type$/i.test(k) &&
          /^(application\/x-www-form-urlencoded|multipart\/form-data|text\/plain)(;.+)?$/i.test(v))
        return original(k, v);
      return;
    });
  }
});

But wait! We can do even better by doing less! If the request isn't even cross-origin in the first place, we don't need to do any of this trickery:

Ajax.Responders.register({
  onCreate: function(response) {
    if (response.request.isSameOrigin())
      return;
    var t = response.transport;
    t.setRequestHeader = t.setRequestHeader.wrap(function(original, k, v) {
      if (/^(accept|accept-language|content-language)$/i.test(k))
        return original(k, v);
      if (/^content-type$/i.test(k) &&
          /^(application\/x-www-form-urlencoded|multipart\/form-data|text\/plain)(;.+)?$/i.test(v))
        return original(k, v);
      return;
    });
  }
});

With this, we've managed to enable proper cross-domain requests in Prototype.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.