Javascript Fetch API: The XMLHttpRequest evolution

The Javascript Fetch API is the current standard to make asynchronous HTTP Requests. To anyone who used XMLHttpRequest and Jquery.ajax(), this new API provides more powerful and flexible features.

The main features of the Javascript Fetch API are:

  • Generic definition of Request and Response objects
    • More control over request properties and content
    • More details about response properties and content.
  • Interaction with other Web APIs (for example: Cache API, Blob API)
  • Better control and setup for CORS-related requests
  • Implemented for Web pages as window.fetch() and for Web workers withWorkerGlobalScope.fetch()

What’s different

Implementation of XMLHttpRequest and fetch() differ in a couple of points, in order to allowfetch()better control over the request and response:

  • Promises returned fromfetch()will not fail in any HTTP error status (even HTTP 404 or 500)
    • It will resolve successfully with ok status set to false
    • It will only fail after a network failure or if the request couldn’t be completed.
  • By default,fetch() will not send cookies or credentials to the server
    • Using the init option, you can specify how to deal with cookies and credentials already stored in the client (omit, include, same-origin)
    • Since Aug 25, 2017. The spec changed the default credentials policy to same-origin.

Usage

The first argument forfetch() is a URL to the resource, and optionally, a second parameter init (an object with a set of request attributes):

var fetchResponsePromise = fetch(URL [, init] );

You can get the same result creating a Request object, then calling fetch()

var request = new Request(URL [, init])
var fetchResponsePromise = fetch(request);

The result is a Promise, which resolve to a Response object. You can chain this call with then() to use the resolved Response and catch() to capture errors in the request/response process. For example, this is a common fetch() call to retrieve and parse JSON data [https://codepen.io/fraigo/pen/GRKdwdR?editors=1111]:

fetch('https://myhost.com/api',{ credentials: 'same-origin' })
  .then(function(response) {
    // use response.text() to continue with plain text
    // verify response checking (response.ok)
    return response.json()
  }).then(function(jsonData) {
    console.log('Parsed JSON', jsonData)
  }).catch(function(ex) {
    console.error('Error', ex.message)
  })

So, after getting the response, it’s converted to a JSON object using response.json(). Then, the JSON object jsonData could be used.  If any error occurs, the exception ex will contain error details. Using ES6 arrow functions syntax is even more clean:

fetch('https://myhost.com/api',{ credentials: 'same-origin' }) 
  .then(response => response.json())
  .then(jsonData => console.log('Parsed JSON', jsonData))  
  .catch(ex => console.error('Error', ex.message))

To see the difference, this is the same process using XMLHtttpRequest [https://codepen.io/fraigo/pen/XWrqyaV?editors=1111]

var httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function(){
  if (httpRequest.readyState === XMLHttpRequest.DONE) {
      if (httpRequest.status === 200) {
        try {
          console.log('Response text', httpRequest.responseText); 
          var jsonData=JSON.parse(httpRequest.responseText);
          console.log('Parsed JSON', jsonData);
        } catch (e) {
          console.error("Parse Error", httpRequest.statusText);
        }
      } else {
        console.error("Response Error", httpRequest.statusText);
      }
  }
};
httpRequest.onerror = function(){
   // will fail in HTTP 500 or 404 as well
   console.error("Request Error");
};
httpRequest.open('GET', 'myhost.com/api');
httpRequest.send();

The main differences are evident. Using XmlHTTPRequest we have:

  • Multiple points of failure/exceptions, different ways to catch them
  • More lines of code, don’t follow a linear sequence
  • One single object to manage the request and the response

In the other hand, using fetch():

  • One point of failure detection, in the promise rejection.
  • Clean code, followed sequentially
  • Different objects to manage requests and responses (including headers and body)

In the next sections I show you the main concepts for the main components of the Fetch API: Request, Response and Headers

Request

The main element in the Javascript Fetch API is the Request. The main properties are:

  • .method (e.g., GET, POST, etc.)
  • .headers (array of Header elements )
  • .credentials (e.g., “omit”, “same-origin”, “include”)
  • .cache (e.g., default, reload, no-cache)
  • .body (request body)
  • .context (e.g., audio, image, iframe, etc.)
  • .mode (e.g., cors, no-cors, same-origin, navigate.)
var request = new Request(
  'https://jsonplaceholder.typicode.com/todos/1',
  { 
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded'},
    credentials: 'same-origin',
    body: 'user=John&email=john@test.com'
  }
);

Headers

Additionally, you can manipulate HTTP headers  (in both Request and Response) for common operations:

  • .append() to create or add a new header (headers with the same name)
  • .delete() to delete a header value
  • .entries() returns an iterator to loop over all headers
  • .forEach() helps to iterate over each header element
  • .get() to get a specific header value
  • .has() allows to test if a header name is set
  • .keys() returns an iterator to loop over all header names

In the next example we set up a request using request Headers.

var myHeaders = new Headers();
myHeaders.append('Content-Type', 'text/json');
myHeaders.append('Authorization', 'Basic QWxhZGRpbjpPcGVuU2VzYW1l');
var myRequest = new Request('https://myhost.com/auth', 
  {headers: myHeaders, method: 'post'})

Also, you can get and manage response Headers:

fetch(myRequest).then(function(response) {
  var resHeaders = response.headers;
  if (resHeaders.has('Auth-token')){
     var myToken = resHeaders.get('Auth-token');
     response.text().then(function(textContent) {
       resultMsg.innerText = textContent;
     });
  }
});

Response

Finally, you can process the Response with a lot of options and methods:

  • .headers Headers object associated with the response
  • .ok Will betrue if the response was successful, with HTTP status 200-299
  • .status HTTP status code, (e.g., 200 for a success, 404 not found)
  • .statusText Text related to the status code  (e.g., OK for 200, Unauthorized for 401)
  • All Body methods, to manage the response content:
    • .text()A Promise to get simple text data
    • .json()To get parsed JSON data in an object
    • .blob()To process Blob data (binary, raw). See Blob
    • .arrayBuffer()To obtain an array of raw bytes. See ArrayBuffer 

In the next example we retrieve binary data in a Blob, and create a data URL to display an image:

fetch('captcha.png', { cache: 'no-cache' })
  .then(function(response) {
    return response.blob();
   }).then(function(blob) {
     const imageURL = URL.createObjectURL(blob);
     captchaImage.src = imageURL;
   });

Compatibility

For those who need backwards compatibility with older browsers with no .fetch() support, you could use a Javascript Fetch API polyfill  (https://github.com/github/fetch).

In some cases you could also need support for promises. A Promise polyfill is available for that case (https://github.com/taylorhakes/promise-polyfill)