Requesting data

Fetch is a popular new Browser API to request data and files. However, older browsers can't deal with fetch(). How do you use it while maintaining compatibility with older browsers?

Progressive enhancement

Instead of writing a polyfill for everything fetch() entails, including Promises, I'd like to build on what's already there in the browser while providing a unified API you can use and expect the same results from. That means XMLHttpRequest() and XDomainRequest(). Implementing then() and catch() is probably beyond the scope of the article, but we'll see where we end up. I'm aiming for IE8+ support here.

The first thing we need to know is what we would expect a request to return as a response. Right? I would expect either a response with the thing I requested, or an error telling me what went wrong.

I think request might be a good name for the API we'll be building, so let's go ahead and create some boilerplate. We need this functionality in the global scope, so I'm putting this in a script tag right in the HTML.


function request() {

};
      

That is very barebones. What else could we need? Some parameters maybe? Both fetch() and XMLHttpRequest() need a URL to be able to do anything, so that seems like a good first candidate. Additionally, both can have options. Now, as fetch() and XMLHttpRequest() handle returning their response in a different way, we need to be able to get those results to where we need them. A single callback is an easy way to do so. Let's add that to the parameters as well.

In addition to defining the parameters, we need to define what they are able to contain. The URL obviously has to be a valid URL. Luckily, both methods will warn you when that's not the case, so we can forget about that. We do need to set some defaults for the options object and make sure they are actually used.

Now, because XMLHttpRequest() can't directly request images (but can insert source URLs which might be the better way to go), we will assume that we are going to either request text or JSON. Of course your text could be a piece of HTML which you would insert with whatever method you'd like.


function request(url, options, callback) {
  var type = options && options.type ? options.type : 'text';

  // XMLHttpRequest only needs the method, but let's deal with that later.
  var opts = Object({
    method: 'GET',
    mode: 'cors',
  });

  for (var key in options) {
    // Don't put the type key in the options for requests
    if (key === 'type') {
      continue;
    }

    opts[key] = options[key];
  }
};
      

For me, writing this with fetch() first is the easy way to go. We should first see if we even have fetch available using feature detection. If fetch is available, we can safely use it to do our request and call the callback when it finishes. Do note the return which stops function execution right there.


function request(url, options, callback) {
  var type = options && options.type ? options.type : 'text';
  var opts = Object({
    method: 'GET',
    mode: 'cors',
  });

  for (var key in options) {
    if (key === 'type') {
      continue;
    }

    opts[key] = options[key];
  }

  // self refers to the Window or Worker object
  if (fetch in self) {
    // now fetch! 🎉
    return fetch(url, options)
      .then(function(response) {
        return response[type]();
      })
      .then(function(content) {
        if (content.cod !== 200) {
          throw Error(content.message);
        }

        return callback(null, content);
      })
      .catch(function(err) {
        console.error(err);
        callback(err);
      })
  }
};
      

Now only if we do not have fetch() available, we ever get below the if-statement. We do a XMLHttpRequest() there, so everyone can enjoy their new content. In the next code snippet, I omitted some parts for brevity.

In IE8 and IE9, you use XDomainRequest() for CORS requests instead of XMLHttpRequest(), so we need to check if it exists and use it if it does.


function request(url, options, callback) {
  … code omitted for brevity …

  // self refers to the Window or Worker object
  if (self && fetch in self) {
    … … code omitted for brevity …
  }

  // XDomainRequest is a IE8 and IE9 specific version of XMLHttpRequest,
  // providing CORS for those browsers.
  var req = self.XDomainRequest ? new self.XDomainRequest() : new XMLHttpRequest();

  req.onerror = function(event) {
    callback(req);
  }

  req.onload = function() {
    if (req.status === 200) {
      var response = type === 'json' ? JSON.parse(req.responseText) : req.responseText;
      return callback(null, response);
    }

    callback(JSON.parse(req.responseText));
  }

  req.open(opts.method, url);

  setTimeout(function() {
    req.send();
  }, 0);
};
      

That setTimeout() is weird, but needed in some scenario's where browsers require req.open() to be executed before req.send() can be. This is it. The whole thing. There you are. Now click the button and see it work.

Unfortunately, the button is broken on HTTPS domains due to fetch()'s security features and would require a proxy server to work on a HTTPS domain. It should work fine on a regular HTTP domain.