XHR Gets a Serious Upgrade

For years, XMLHttpRequest (XHR) was the workhorse of dynamic web apps, but it had distinct limitations. Downloading binary data required server-side tricks with mime types, sending anything but plain text or XML was a struggle, and cross-origin requests were effectively blocked. The XMLHttpRequest Level 2 specification addresses these gaps with a set of improvements that make the API far more practical for modern applications, particularly when working with files and rich media.

Requesting Data in the Format You Need

The classic workaround for fetching a binary file was to override the server's mime type and then parse the resulting text string, character by character. While this hack worked, it was brittle and inefficient; you never got a true binary object.

XHR2 introduces two properties—responseType and response—that let the browser handle the dirty work. Before sending a request, you set xhr.responseType to one of four values: "text", "arraybuffer", "blob", or "document". After a successful request, the response property holds the data in the corresponding format. Omitting responseType (or setting it to an empty string) causes the response to default to a DOMString.

Fetching an image as a binary blob, for instance, now takes just a few lines of code to request it as a true Blob object rather than a mangled string:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    // Note: .response instead of .responseText
    var blob = new Blob([this.response], {type: 'image/png'});
    ...
  }
};

xhr.send();

Working with ArrayBuffers

An ArrayBuffer is a generic, fixed-length container for raw binary data. Its real power comes from the ability to create "views" of the underlying data using JavaScript typed arrays, such as an unsigned 8-bit integer array. Multiple views can point to the same buffer, allowing you to interpret the same bytes in different ways without copying data.

The following example fetches an image as an ArrayBuffer and creates a Uint8Array view over that data:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response); // this.response == uInt8Array.buffer
  // var byte3 = uInt8Array[4]; // byte at offset 4
  ...
};

xhr.send();

Direct Blob Handling

If your code does not require byte-level manipulation, you can request the server's response directly as a Blob. This format is useful when you want to store the data in IndexedDB, write it to the HTML5 File System, or generate an object URL for it.

window.URL = window.URL || window.webkitURL;  // Take care of vendor prefixes.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;

    var img = document.createElement('img');
    img.onload = function(e) {
      window.URL.revokeObjectURL(img.src); // Clean up after yourself.
    };
    img.src = window.URL.createObjectURL(blob);
    document.body.appendChild(img);
    ...
  }
};

xhr.send();

Richer Payloads for Sending

The send() method has also been expanded. It now accepts not just DOMString and Document, but also FormData, Blob, File, and ArrayBuffer. Sending a plain string is unchanged, while setting responseType='text' after the call ensures a text response for comparison.

Simplified Form Submissions

A major convenience is the native FormData type, which allows you to construct an HTML form in JavaScript and submit it via AJAX without a library.

function sendForm() {
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);
}

You don't have to start from scratch. A FormData object can be populated from an existing form element on the page:

<form id="myform" name="myform" action="/server">
  <input type="text" name="username" value="johndoe">
  <input type="number" name="id" value="123456">
  <input type="submit" onclick="return sendForm(this.form);">
</form>

If that form includes file inputs, the browser handles the file data transparently and constructs a proper multipart/form-data request when send() is called.

function uploadFiles(url, files) {
  var formData = new FormData();

  for (var i = 0, file; file = files[i]; ++i) {
    formData.append(file.name, file);
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);  // multipart/form-data
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  uploadFiles('/server', this.files);
}, false);

Uploading Files and Raw Bytes

You can upload a Blob (and since all Files are Blobs, this covers file uploads as well). The example below creates a text file on the fly using the Blob() constructor, uploads it, and uses progress events to report on the upload's status.

<progress min="0" max="100" value="0">0% complete</progress>

For lower-level work, you can also send a chunk of raw data as an ArrayBuffer.

function sendArrayBuffer() {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  var uInt8Array = new Uint8Array([1, 2, 3]);

  xhr.send(uInt8Array.buffer);
}

Cross-Origin Requests Without Pain

Cross-Origin Resource Sharing (CORS) finally provides a standardized way for a web app on one domain to make an AJAX request to another domain. Server-side, enabling it is a matter of adding a response header. To allow a specific origin:

Access-Control-Allow-Origin: http://example.com

To allow access from any domain:

Access-Control-Allow-Origin: *

Once a server has set this header, any other web page can interact with it. The client-side code looks identical to a same-origin request:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

Practical Demonstrations

Downloading Files for Local Storage

XHR2's blob support pairs well with the HTML5 File System. You can fetch images, retrieve them as Blobs, and store them locally using the FileWriter API without any encoding hacks.

window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

function onError(e) {
  console.log('Error', e);
}

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {

  window.requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
    fs.root.getFile('image.png', {create: true}, function(fileEntry) {
      fileEntry.createWriter(function(writer) {

        writer.onwrite = function(e) { ... };
        writer.onerror = function(e) { ... };

        var blob = new Blob([xhr.response], {type: 'image/png'});

        writer.write(blob);

      }, onError);
    }, onError);
  }, onError);
};

xhr.send();

Large Uploads via Chunking

For large files, the File API's slice method can be used to break an upload into manageable pieces. The following code spawns a dedicated XHR request for each slice, a technique that can help evade server-side request size limits.

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };
  xhr.send(blobOrFile);
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  var blob = this.files[0];

  const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {
    upload(blob.slice(start, end));

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

})();

Note that the server-side logic to reassemble the chunks is not included in the example.