A Minimal Javascript HTTP Abstraction

What’s the minimum amount of code required to make XMLHttpRequest more concise?

I needed a javascript HTTP abstraction for Shrewdness (my reader, current project), and wanted to use something more futureproof than github.com/stackp/promisejs, as their Promise implementation differs from the bleeding-edge Promise standard.

After looking over the updates to XMLHttpRequest, it turns out that adding some syntatic sugar and Promise-wrapping is all that’s necessary to create a HTTP abstraction which is minimal and memorable whilst retaining all the flexibility of the “low-level” XMLHttpRequest API.

Right now, using said low-level API looks like this:

var request = new XMLHttpRequest();
request.open('POST', '/notes/new/');

request.onload = function () {
    if (request.status - 200 <= 99 && request.status - 200 > -1) {
        // The request was successful.
        request.responseText;
        request.getResponseHeader('Content-length');
    } else {
        // The request failed.
    }
};

request.onerror = function () {
    // The request failed in a different way.
};

var data = new FormData();
data.append('content', 'A new note, posted using XMLHttpRequest');

request.send(data);

The amount of flexibility that XMLHttpRequest gives us is quite impressive — we can make any sort of request to any URL, send all sorts of different request bodies, manipulate request and response headers, perform different actions on success, failure or network-level failure, monitor its state closely with onreadystatechange and so on (check out the documentation for more, I guarantee there’ll be something in there you didn’t know XMLHttpRequest could do).

But there are some issues as well — for a start, that’s a lot of code to just make one request. There are two different places where error handling needs to take place, an ugly, empty constructor call which is for some reason separate from the open() method, doubling the surface area and quadrupling the amount of things developers have to remember. There’s also the counter-intuitive fact that the code for handling what happens when the request is complete is defined before the request is sent.

Some improvements can be made here, without sacrificing any of the flexibility and power XMLHttpRequest gives us.

Let’s start with the constructor, by getting rid of it and replacing it with the part which actually does something useful, the open() method.

var xhr = http.open('POST', '/notes/new/');

Much better! The memorable, familiar open() is no longer a mere method of XMLHttpRequest, but the entry point function through which an object representing the request+response is created.

There’s not much point creating abstractions for manipulating the request as the XMLHttpRequest interface does a perfectly good job, but sending the request and handling its outcome could do with some improvement. Let’s make send() return a Promise, so we can write code like this:

http.send(xhr).then(function () {
    // Success! Do stuff with xhr.
}, function () {
    // Failure :( Report the error in some way.
});

send() would work just like XMLHttpRequest.send() does, accepting a value for the body of the request as the second parameter. One possible additional piece of syntatic sugar would be to accept a plain object and convert it to a FormData instance, or indeed to accept an HTMLFormElement and turn it into a FormData.

The fact that send() now returns a Promise means not only that adding success/failure handlers is trivial and standardised, but also that we can do all sorts of fun stuff like this:

var xhrs = [
    http.send(http.open('GET', '/page/1')),
    http.send(http.open('GET', '/page/2')),
    http.send(http.open('GET', '/page/3'))
];

Promise.all(xhrs).then(function () {
    // All of the requests completed.
}, function () {
    // At least some of the requests failed :(
});

For more on the interesting stuff which can be done with Promises, check out Jake Archibald’s article on Javascript Promises.

An implementation of the abstractions outlined above is trivial to create — here’s all 24 lines of mine at the time of writing (using AMD):

'use strict';

define({
    open: function open(method, url) {
        var xhr = new XMLHttpRequest();
        xhr.open(method.toUpperCase(), url);
        return xhr;
    },

    send: function send(xhr, value) {
        var value = value || null;
        return new Promise(function (resolve, reject) {
            xhr.onload = function () {
                // Success if status in 2XX.
                if (xhr.status - 200 <= 99 && xhr.status - 200 > -1) {
                    resolve(xhr);
                } else {
                    reject(xhr);
                }
            };

            xhr.onerror = function () {
                reject(xhr);
            };

            xhr.send(value);
        });
    }
});

There are a few things which could easily be added to this for further improvements:

  • Implement the send() request body transformations outlined above
  • Consider 1XX and 3XX responses to be successes as well as 2XX
  • Make http.send() check the state of an xhr before sending, perhaps storing the Promise on the xhr so that http.send(xhr) can be called multiple times, only ever make the request once and always return the same promise.

I asked around in the WHATWG IRC channel about the future of XMLHttpRequest and native Promise support. Nothing is particularly solid yet, but the places to look are the Fetch Standard and the XMLHttpRequest Standard.

Interestingly it looks like the Fetch standard splits it up into Request and Response objects — I’m not sure how I feel about this.