hacks.mozilla.org

Archive for the 'XMLHttpRequest' Category

cross-site xmlhttprequest with CORS

XMLHttpRequest is used within many Ajax libraries, but till the release of browsers such as Firefox 3.5 and Safari 4 has only been usable within the framework of the same-origin policy for JavaScript. This meant that a web application using XMLHttpRequest could only make HTTP requests to the domain it was loaded from, and not to other domains. Developers expressed the desire to safely evolve capabilities such as XMLHttpRequest to make cross-site requests, for better, safer mash-ups within web applications. The Cross-Origin Resource Sharing (CORS) specification consists of a simple header exchange between client-and-server, and is used by IE8’s proprietary XDomainRequest object as well as by XMLHttpRequest in browsers such as Firefox 3.5 and Safari 4 to make cross-site requests. These browsers make it possible to make asynchronous HTTP calls within script to other domains, provided the resources being retrieved are returned with the appropriate CORS headers.

A Quick Overview of CORS

Firefox 3.5 and Safari 4 implement the CORS specification, using XMLHttpRequest as an “API container” that sends and receives the appropriate headers on behalf of the web developer, thus allowing cross-site requests. IE8 implements part of the CORS specification, using XDomainRequest as a similar “API container” for CORS, enabling simple cross-site GET and POST requests. Notably, these browsers send the ORIGIN header, which provides the scheme (http:// or https://) and the domain of the page that is making the cross-site request. Server developers have to ensure that they send the right headers back, notably the Access-Control-Allow-Origin header for the ORIGIN in question (or ” * ” for all domains, if the resource is public) .

The CORS standard works by adding new HTTP headers that allow servers to serve resources to permitted origin domains. Browsers support these headers and enforce the restrictions they establish. Additionally, for HTTP request methods that can cause side-effects on user data (in particular, for HTTP methods other than GET, or for POST usage with certain MIME types), the specification mandates that browsers “preflight” the request, soliciting supported methods from the server with an HTTP OPTIONS request header, and then, upon “approval” from the server, sending the actual request with the actual HTTP request method. Servers can also notify clients whether “credentials” (including Cookies and HTTP Authentication data) should be sent with requests.

Capability Detection

XMLHttpRequest can make cross-site requests in Firefox 3.5 and in Safari 4; cross-site requests in previous versions of these browsers will fail. It is always possible to try to initiate the cross-site request first, and if it fails, to conclude that the browser in question cannot handle cross-site requests from XMLHttpRequest (based on handling failure conditions or exceptions, e.g. not getting a 200 status code back). In Firefox 3.5 and Safari 4, a cross-site XMLHttpRequest will not successfully obtain the resource if the server doesn’t provide the appropriate CORS headers (notably the Access-Control-Allow-Origin header) back with the resource, although the request will go through. And in older browsers, an attempt to make a cross-site XMLHttpRequest will simply fail (a request won’t be sent at all).

Both Safari 4 and Firefox 3.5 provide the withCredentials property on XMLHttpRequest in keeping with the emerging XMLHttpRequest Level 2 specification, and this can be used to detect an XMLHttpRequest object that implements CORS (and thus allows cross-site requests). This allows for a convenient “object detection” mechanism:

if (XMLHttpRequest)
{
    var request = new XMLHttpRequest();
    if (request.withCredentials !== undefined)
    {
      // make cross-site requests
    }
}

Alternatively, you can also use the “in” operator:

if("withCredentials" in request)
{
  // make cross-site requests
}

Thus, the withCredentials property can be used in the context of capability detection. We’ll discuss the use of “withCredentials” as a means to send Cookies and HTTP-Auth data to sites later on in this article.

“Simple” Requests using GET or POST

IE8, Safari 4, and Firefox 3.5 allow simple GET and POST cross-site requests. “Simple” requests don’t set custom headers, and the request body only uses plain text (namely, the text/plain Content-Type).

Let us assume the following code snippet is served from a page on site http://foo.example and is making a call to http://bar.other:

 
var url = "http://bar.other/publicNotaries/"
if(XMLHttpRequest)
{
  var request = new XMLHttpRequest();
  if("withCredentials" in request)
  {
   // Firefox 3.5 and Safari 4
   request.open('GET', url, true);
   request.onreadystatechange = handler;
   request.send();
  }
  else if (XDomainRequest)
  {
   // IE8
   var xdr = new XDomainRequest();
   xdr.open("get", url);
   xdr.send();
 
   // handle XDR responses -- not shown here :-)
  }
 
 // This version of XHR does not support CORS  
 // Handle accordingly
}

Firefox 3.5, IE8, and Safari 4 take care of sending and receiving the right headers. Here is the Simple Request example. It is also instructive to look at the headers sent back by the server. Notably, amongst the other request headers, the browser would send the following in order to enable the simple request above:

GET /publicNotaries/ HTTP/1.1
Referer: http://foo.example/notary-mashup/
Origin: http://foo.example

Note the use of the “Origin” HTTP header that is part of the CORS specification.

And, amongst the other response headers, the server at http://bar.other would include:

Access-Control-Allow-Origin: http://foo.example
Content-Type: application/xml
......

A more complete treatment of CORS and XMLHttpRequest can be found here, on the Mozilla Developer Wiki.

“Preflighted” Request

The CORS specification mandates that requests that use methods other than POST or GET, or that use custom headers, or request bodies other than text/plain, are preflighted. A preflighted request first sends the OPTIONS header to the resource on the other domain, to check and see if the actual request is safe to send. This capability is currently not supported by IE8’s XDomainRequest object, but is supported by Firefox 3.5 and Safari 4 with XMLHttpRequest. The web developer does not need to worry about the mechanics of preflighting, since the implementation handles that.

The code snippet below shows code from a web page on http://foo.example calling a resource on http://bar.other. For simplicity, we leave out the section on object and capability detection, since we’ve covered that already:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '
Arun';
function callOtherDomain(){
if(invocation)
{
    invocation.open('POST', url, true);
    invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
    invocation.setRequestHeader('Content-Type', 'application/xml');
    invocation.onreadystatechange = handler;
    invocation.send(body);
}

You can see this example in action here. Looking at the header exchange between client and server is really instructive. A more detailed treatment of this can be found on the Mozilla Developer Wiki.

In this case, before Firefox 3.5 sends the request, it first uses the OPTIONS header:

OPTIONS /resources/post-here/ HTTP/1.1
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

Then, amongst the other response headers, the server responds with:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://arunranga.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER
Access-Control-Max-Age: 1728000

At which point, the actual response is sent:

POST /resources/post-here/ HTTP/1.1
...
Content-Type: application/xml; charset=UTF-8
X-PINGOTHER: pingpong
...

Credentialed Requests

By default, “credentials” such as Cookies and HTTP Auth information are not sent in cross-site requests using XMLHttpRequest. In order to send them, you have to set the withCredentials property of the XMLHttpRequest object. This is a new property introduced in Firefox 3.5 and Safari 4. IE8’s XDomainRequest object does not have this capability.

Again, let us assume some JavaScript on a page on http://foo.example wishes to call a resource on http://bar.other and send Cookies with the request, such that the response is cognizant of Cookies the user may have acquired.

var request = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain(){
  if(request)
  {
   request.open('GET', url, true);
   request.withCredentials = "true";
   request.onreadystatechange = handler;
   request.send();
  }

Note that withCredentials is false (and NOT set) by default. The header exchange is similar to the case of of a simple GET request, with the exception that now an HTTP Cookie header is sent with the request header. You can see this sample in action here.

A Note on Security

In general, data requested from a remote site should be treated as untrusted. Executing JavaScript code retrieved from a third-party site without first determining its validity is NOT recommended. Server administrators should be careful about leaking private data, and should judiciously determine that resources can be called in a cross-site manner.

References

connecting html5 video to the web

This is a screencast of a demo that I gave at the open video conference in NYC on June 19th, 2009. The demo itself was created by the ever wonderful Paul Rouget.

Download .ogv or .mp4 version. A version hosted on blip.tv.

XHR progress and rich file upload feedback

This demo is by Olli Pettay (smaug) with help from Austin King.

A common limitation on the web today has been a rich file upload widget for web applications. Many sites use Flash or a desktop helper applications to improve the experience of uploading files.

Firefox 3.5 bridges one of these gaps allowing a better progress indicator to be built. Many developers don’t realize that they can use Firefox’s File object (nsIDOMFile) and XMLHttpRequest together to accomplish file uploads. This demo will feature an upload widget that gives the kind of rich progress feedback that users have come to expect, as well as fast and easy multiple simultaneous file uploads.

Progress Indicators

It’s always a good idea to expose feedback that your application is hard at work for them, and when the current action is expected to finish. The two main types of progress feedback are:

  • indeterminate progress – some activity just happened
  • deterministic progress I’m 40% done, I’m 42% done… etc

Deterministicsaidwhat? The Demo

We’ve created a simple file upload / download page that demonstrates the progress bar:

The demo is host at mozilla.pettay.fi and requires Firefox 3.5 beta4 or later. It demonstrates how to do multiple simultaneous file uploads without posting a form or leaving the current page. For each file upload / download we display the current speed, % complete, and bytes transmitted. We’ll go over a few key snippets of the code which are used in the screenshot above. Please click through the the demo and view source for the full code example.

The page contains two HTML inputs, one type="file" and one type="button". The form is never actually submitted, instead we add an onclick handler to the button:

<input type="file" id="file">
<input type="button"
          onclick="startXHR();"
          value="Upload file using XHR">

In the startXHR function, we create an XMLHttpRequest and add an event handler to the XHR request to listen for the new ‘progress’ event. With this ProgressEvent’s lengthComputable property, we will know if we are dealing with an indeterminate or deterministic progress. The object also gives us loaded and total bytes.

var xhr = new XMLHttpRequest();
 
xhr.onprogress = function(evt) {        
if (evt.lengthComputable) {
    evt.target.curLoad = evt.loaded;
    evt.target.log.parentNode.parentNode.previousSibling.textContent =
        Number(evt.loaded/k).toFixed() + "/"+ Number(evt.total/k).toFixed() + "kB";
}
if (evt.lengthComputable) {
    var loaded = (evt.loaded / evt.total);
    if (loaded < 1) {
        var newW = loaded * width;
        if (newW < 10) newW = 10;
            evt.target.log.style.width = newW + "px";
        }
    }
};

Now we need some data to send. We grab the contents of the file directly from the input by id:

var files = document.getElementById("file").files;
if (files) {
   var file = files.item(0);
   xhr.sendAsBinary(file.getAsBinary());

And the last step is to start the request:

xhr.open("POST", "cgi-bin/posthandler.pl");
xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');
xhr.sendAsBinary(file.getAsBinary());

These methods would also work with xhr.upload.onprogress.

Notice the use of the sendAsBinary method on the XMLHttpRequest object and getAsBinary on the File object. Starting with Firefox 3 you’ve been able to get at the contents of a file on the client side without form submission. This is quite useful for moving beyond the limitation of tranditional file input and form submissions. It is also part of an up and coming W3C standard for FileUpload.

A related method that the nsIDOMFile provides is the getAsText method which returns a DOMString suitable for slicing, dicing, and displaying.

Here is an example usage, not used by the demo:

file.getAsText("utf8");

So that’s the gist of the code. Check out the demo and view it’s source.

Feedback In User Interfaces

Exposing system feedback to users improves perceived performance. We
can’t always determine how long something will take, so at a minimum we
can show indeterminate progress.

During file uploads and file downloads (assuming the server gives us Content-Length) we do indeed know the total number of bytes. Firefox 3.5’s Progress Events support adds a progress event so that we can show actual upload/download progress.

Traditionally XMLHttpRequests were difficult to get deterministic progress back from. In theory, you could give it callbacks and watch for status code updates and textual message updates, but in practice they turn out to be not very useful. In the past, if a deterministic progress meter was important, you’d have to make a second XHR request to poll for progress.

Enter Progress Events

The W3C has a working draft for Progress Events 1.0 which we include in Firefox 3.5. Firefox has added a key new DOM ProgressEvent progress event, as well as the loadstart event. The other existing events included: error, abort and load.

These same events are also available for uploads and downloads. The progress event gives us the following properties:

  • lengthComputable – true or false, is the size of the request known?
  • loaded – number of bytes received so far
  • total – number of bytes expected for entire request

The Contract

When you’re looking at the properties of those progress events, certain rules apply that you can depend on. They are:

  • The total property will be 0 when lengthComputable is false.
  • The loadstart event is always signaled only once.
  • The progress event is fired off zero or more times after loadstart.

And that’s it. Go forth and improve file uploads with Firefox 3.5 goodness.