XMLHttpRequest

XMLHttpRequest is a built-in browser object that allows to make HTTP requests in JavaScript. Despite of having the word “XML” in its name, it can operate on any data, not only in XML format.

XMLHttpRequest has two modes of operation: synchronous and asynchronous. First let’s see the asynchronous variant as it’s used in the majority of cases.

The code below loads the URL at /article/xmlhttprequest/hello.txt from the server and shows its content on-screen:

// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../hello.txt
xhr.open('GET', '/article/xmlhttprequest/hello.txt');

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function() {
  if (xhr.status != 200) { // analyze HTTP status of the response
    // if it's not 200, consider it an error
    alert(xhr.status + ': ' + xhr.statusText); // e.g. 404: Not Found
  } else {
    // show the result
    alert(xhr.responseText); // responseText is the server response
  }
};

As we can see, there are several methods of XMLHttpRequest here. Let’s cover them.

Open

xhr.open(method, URL, async, user, password)

This method is usually called first after new XMLHttpRequest. It specifies the main parameters of the request:

Please note that open call, contrary to its name, does not open the connection. It only configures the request, but the network activity only starts with the call of send.

Send

xhr.send([body])

This method opens the connection and sends the request to server. The optional body parameter contains the request body. Some request methods like GET do not have a body. And some of them like POST use body to send the data.

Cancel Request

abort()

If we changed our mind, we can terminate the request at any time. The call to xhr.abort() does that:

xhr.abort(); // terminate the request

timeout

We can also specify a timeout using the corresponding property:

xhr.timeout = 10000;

The timeout is expressed in ms. If the request does not succeed within the given time, it gets canceled automatically.

The maximum duration of an asynchronous request can be set using the timeout property:

xhr.timeout = 30000; // 30 seconds (in milliseconds)

If the request exceeds that time, it’s aborted, and the timeout event is generated:

xhr.ontimeout = function() {
  alert( 'Sorry, the request took too long.' );
}

Events

A request is asynchronous by default. In other words, the browser sends it out and allows other JavaScript code to execute. After the request is sent, xhr starts to generate events. We can use addEventListener or on<event> properties to handle them, just like with DOM objects.

Here are useful events associated with XMLHttpRequest object:

Using these events we can track successful loading (onload), errors (onerror) and the amount of the data loaded (onprogress).

Please note that errors here are “communication errors”. In other words, if the connection is lost or the remote server does not respond at all – then it’s the error in the terms of XMLHttpRequest. Bad HTTP status like 500 or 404 are not considered errors.

Here is another example:

<script>
  function load(url) {
    let xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.timeout = 1000;
    xhr.send();

    xhr.onload = function() {
      alert(`Loaded: ${this.status} ${this.responseText}`);
    };

    xhr.onerror = () => alert('Error');

    xhr.ontimeout = () => alert('Timeout!');
  }
</script>

<button onclick="load('/article/xmlhttprequest/hello.txt')">Load</button>
<button onclick="load('/article/xmlhttprequest/hello.txt?speed=0')">Load with timeout</button>
<button onclick="load('no-such-page')">Load 404</button>
<button onclick="load('http://example.com')">Load another domain</button>

Response

Once the server has responded, we can receive the result in the following properties of the request object:

Synchronous Request

If in the open method the third parameter async is set to false, the request is made synchronously. In other words, Javascript execution pauses at that line and continues when the response is received. Somewhat like alert or prompt commands.

Synchronous calls are used rarely, because they block in-page Javascript till the loading is complete. In some browsers, a user is unable to scroll the page.

// Synchronous request
xhr.open('GET', 'phones.json', false);

// Send it
xhr.send();
// ...JavaScript "hangs" and waits till the request is complete

If a synchronous call takes too much time, the browser may suggest to close the “hanging” webpage.

Also, because of the blocking, it becomes impossible to send two requests simultaneously. And, looking a bit forward, let’s note that some advanced capabilities of XMLHttpRequest, like requesting from another domain or specifying a timeout, are unavailable for synchronous requests.

Because of all that, synchronous requests are used very sparingly, almost never. By default, requests are asynchronous.

Event “readystatechange”

The event readystatechange occurs multiple times during sending the request and receiving the response.

As the name suggests, there’s a “ready state” of XMLHttpRequest. It is accessible as xhr.readyState.

In the example above we only used state 4 (request complete), but there are few more.

const unsigned short UNSENT = 0; // initial state
const unsigned short OPENED = 1; // open called
const unsigned short HEADERS_RECEIVED = 2; // response headers received
const unsigned short LOADING = 3; // response is loading (a data packed is received)
const unsigned short DONE = 4; // request complete

An XMLHttpRequest object travels them in the order 0 → 1 → 2 → 3 → … → 3 → 4. State 3 repeats every time a data packet is received over the network.

Here is an example of that event:

let xhr = new XMLHttpRequest();

xhr.open('GET', 'phones.json'); // the third parameter is true by default

xhr.send(); // (1)

xhr.onreadystatechange = function() { // (3)
  if (xhr.readyState != 4) return;

  button.innerHTML = 'Complete!';

  if (xhr.status != 200) {
    alert(xhr.status + ': ' + xhr.statusText);
  } else {
    alert(xhr.responseText);
  }

}

button.innerHTML = 'Loading...'; // (2)
button.disabled = true;

Historically, the event readystatechange appeared long ago, before the specification settled. Nowadays, there’s no need to use it, we can replace it with other available events, but it can often be found in older scripts.

HTTP-Headers

XMLHttpRequest allows both to send custom headers and read headers from the response. There are 3 methods for HTTP-headers:

setRequestHeader(name, value)

Sets the request header with the given name and value.

xhr.setRequestHeader('Content-Type', 'application/json');

getResponseHeader(name)

Gets the response header with the given name (except Set-Cookie and Set-Cookie2).

xhr.getResponseHeader('Content-Type')

getAllResponseHeaders()

Returns all response headers, except Set-Cookie and Set-Cookie2.

xhr.getAllResponseHeaders();

Headers are returned as a single line, e.g.:

Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT

The line break between headers is always "\r\n" (doesn’t depend on OS), so we can easily split it into individual headers. The separator between the name and the value is always a colon followed by a space ": ". That’s fixed in the specification.

So, if we want to get an object with name/value pairs, we need to throw in a bit JS.

let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

Headers limitations

Several headers are managed exclusively by the browser, e.g. Referer and Host. The full list is in the specification. XMLHttpRequest is not allowed to change them, for the sake of user safety and correctness of the request.

Can’t remove a header

Another peciliarity of XMLHttpRequest is that one can’t undo setRequestHeader. Once the header is set, it’s set. Additional calls to setRequestHeader append information to the header, it doesn't overwrite it.

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// the header will be:
// X-Auth: 123, 456