To fetch the content of a URI using an XMLHttpRequest object in JavaScript.
XMLHttpRequest is the JavaScript class which underpins the concept of AJAX (Asynchronous JavaScript and XML). It allows a web page to load new data without reloading the page as a whole. This in turn allows many important types of web application to be coded entirely in JavaScript when it would previously have been necessary to rely on technologies such as Java applets or ActiveX.
Despite its name XMLHttpRequest is not limited to XML: it can be used to fetch any type of data, including other text-based formats such as JSON, and binary formats such as PNG or JPEG.
It is good practice to transfer data in structured form where possible, in order to cleanly separate the presentation layer (on the browser) from the business logic (on the server). Alternatively you can take the more direct approach of sending pre-formatted HTML or XHTML, which need only be parsed by the browser before being inserted into the document tree.
Suppose you are developing a webmail application, and want the client to list the content of the user’s inbox. It can obtain the necessary raw data from the server by making a GET request to the URL /folders/inbox, accompanied by a pre-existing cookie which is used to identify and authenticate the user. The response to the GET request is an array of objects encoded as JSON.
The method described here has six steps:
The constructor should normally be called without any arguments:
var xhr = new XMLHttpRequest(); if (!xhr) { alert('failed to create XMLHttpRequest object'); }
For subsequent requests you can choose between creating a new XMLHttpRequest object or reusing an old one (see below).
The XMLHttpRequest constructor returns a request object which is in the UNSENT state. In order to progress to the OPENED state you must specify the URI to be fetched and HTTP method to be used. This is done by calling the open method:
xhr.open("GET", "/folders/inbox", true);
There are two required parameters and three optional ones:
The URI can be given in absolute or relative form, but if the request is made in the context of a web page then it must either conform to the same-origin policy [http://tools.ietf.org/html/rfc6454] or be allowed on some other basis such as CORS [http://www.w3.org/TR/cors/] (see below). The W3C standard requires support for the http and https URI schemes only. Where other schemes are supported by the browser there are likely to be differences in behaviour (for example, due to not using HTTP headers and status codes).
Setting the third argument to true indicates that the transaction should be completed asynchronously, meaning that send should return immediately when called without waiting for a response from the HTTP server. Synchronous requests are strongly discouraged when running in the foreground context of a web page, because the user interface is single-threaded and will become unresponsive if you cause the foreground thread to block.
You may encounter code where the third argument has been omitted. This is fully acceptable, since it is an optional argument with a default value of true, however it is common practice to specify the required value explicitly.
The username and password arguments are relevant when using HTTP authentication methods which require them, such as Basic Authentication or Digest Authentication. They can be omitted or set to null to indicate that no credentials are available. If authentication is necessary to gain access to the requested URI then omission can result in the user being prompted by the web browser. If you do not want that to happen then prompting can be suppressed by supplying an invalid username and password. These will either be ignored, or result in an 401 (Unauthorized) response.
The default behaviour of XMLHttpRequest is to expect a text response from the remote server and present it to the caller in the form of a DOMString. Since JSON is a text-based data format, one option is to receive it as text then parse it afterwards, however modern web browsers provide a shortcut with the potential for greater efficiency. By setting its responseType property, you can inform the XMLHttpRequest object that a JSON-encoded response is expected:
xhr.responseType = 'json';
This feature is not supported by all web browsers, but you can allow for that by providing a fallback mechanism which decodes by another method if necessary. To prepare for this it is helpful to:
You can test whether the request object has a responseType property using the in operator:
try { if ('responseType' in xhr) { xhr.responseType = 'json'; } } catch (e) {}
Response types allowed by the XMLHttpRequest specification are:
responseType | decoded | parsed | response |
---|---|---|---|
arraybuffer | no | no | ArrayBuffer |
blob | no | no | Blob |
document | yes | as HTML or XML | Document |
json | yes | as JSON | (varies) |
text | yes | no | DOMString |
The default value (which is the empty string) has similar behaviour to text, but with some additional rules for determining the character set of XML documents.
Because the HTTP request occurs asynchronously, you will normally want to be notified when it has finished. The most portable way to do this is by means of the onreadystatechange event handler. This refers to the readyState attribute of the XMLHttpRequest object, which can take the following values:
0 | UNSENT | The object has been constructed. |
1 | OPENED | The open method has been successfully called. |
2 | HEADERS_RECEIVED | Reception of response headers is complete. |
3 | LOADING | Reception of response body is in progress. |
4 | DONE | Reception of response has either completed or failed. |
Of these, the state most likely to be of interest is DONE. Since it does not by itself distinguish between success and failure, it is then necessary to check the HTTP status code. This can be found in the status attribute. Values in the range 200 to 299 indicate that the request was successful:
xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300) { var folderIndex = (xhr.responseType == 'json') ? xhr.response : JSON.parse(xhr.responseText); processFolderIndex(folderIndex); } else { alert('failed to download folder index'); } xhr.onreadystatechange = null; } }
You may encounter scripts which treat a status code of 200 as the only successful outcome. This will work under most circumstances, since the other codes in that range are rarely used, but there is some risk of unnecessarily rejecting a usable response.
The reason for resetting the event handler is to avoid a race condition which might otherwise cause the data to be processed more than once. It is necessary because the value of readyState seen by the handler could be different from the value it had when the event was triggered. The reset also avoids a memory leak in some browsers.
Fallback to JSON.parse is implemented here by testing whether the responseType was successfully set to JSON. As previously noted, if this feature is supported at all then it should only allow supported response types to be set. You may wish to provide a further fallback to eval in case JSON.parse is not available either.
Prior to this point, nothing has been sent to the remote server. To make that happen you should call the send function:
xhr.send();
This has one optional parameter, which is a message body to be sent as part of the request:
xhr.send(body);
Whether it is appropriate to send a message body, and what it should contain, will depend on the HTTP request method and the URI. See below for further details.
There is a bug in FireFox 3.0 and below which causes send to throw an exception if you do not supply any arguments. You can work around this by explicitly setting the message body to null:
xhr.send(null);
Provided you did not ask for a synchronous connection, the send function should return almost immediately. Your script can then go on to do other work if required, but it should ultimately return control to whatever called it within a reasonably short period of time. The event handler will be called as and when appropriate without any further action on the part of your script.
var xhr = new XMLHttpRequest(); if (!xhr) { alert('failed to create XMLHttpRequest object'); } xhr.open("GET", "/folders/inbox", true); try { if ('responseType' in xhr) { xhr.responseType = 'json'; } } catch (e) {} xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300) { var folderIndex = (xhr.responseType == 'json') ? xhr.response : JSON.parse(xhr.responseText); processFolderIndex(folderIndex); } else { alert('failed to download folder index'); } xhr.onreadystatechange = null; } } xhr.send(null);
The four key methods for implementing a RESTful web service are:
In the absence of a more suitable method, normal practice is to use POST for operations with side effects and GET for operations with none. Other useful methods include:
Typically only a subset of these methods will be available at any given URI. You can use any method that the HTTP server supports, with the exception of CONNECT, TRACE and TRACK (which are forbidden for security reasons). Beware that even if the web service disregards the HTTP method (and therefore accepts any method), the choice that you make can still influence how requests and responses are cached.
Whether an HTTP request can or should have a message body, and what the message body should contain, is determined in part by the request method:
Beyond this the HTTP specification places few restrictions on how the message body is used. For this reason, web services are usually accompanied by some form of API specification detailing what should be sent and how.
Where a message body of type application/x-www-form-urlencoded or multipart/form-data is needed, options include:
Note that provision of a message body does not preclude you from also encoding information into the path and/or query components of the URI if it is semantically appropriate to do so. (This would not be possible with an HTML form, but there is no objection within the HTTP specification.)
When XMLHttpRequest is used from within a web page, it cannot be used to access arbitrary URIs. The reason for this is to limit the potential for abuse. Without any restrictions an attacker would be able to:
To prevent this, browsers check whether the request is directed back to the site from which the web page originated. If so then the request is allowed, on the basis that the website owner has responsibility for ensuring that pages within the website do not invoke scripts which put the website at risk. The details of this policy are documented in RFC 6454 [https://www.ietf.org/rfc/rfc6454.txt]. For URIs which contain a hostname there are three components which must match:
Both the URI scheme and hostname are normalised to lower case before comparison. The appropriate default port is used if none has been specified. For URIs without a hostname there is significant variation between browsers, and you should not assume that any access will be available.
Points to note:
Consequences of the same-origin policy are that web applications cannot easily combine data from multiple servers, or be served separately from any web services that they make use of. This is problematic because there is often a need to separate these types of subsystem for security or performance reasons. An exception to the same-origin policy has therefore been defined, known as Cross-Origin Resource Sharing (CORS).
CORS allows safe requests (such as GET and HEAD) to be made to any destination, but does not allow the script to see the result unless the server responds with a suitable Access-Control-Allow-Origin header. The same applies to some less-than-safe POST requests, which are allowed due to pre-existing loopholes. Other types of request are permitted, but are validated with the server before they are sent (a process known as preflight).
If CORS is unavailable then there are some alternative methods which can be used to circumvent the same-origin policy and achieve a similar outcome:
If a web server outside your control fails to set appropriate cache control headers then it is usually possible to prevent caching by adding a dummy parameter with a random value to the query component of the URI. For example, the URI /folders/inbox might become /folders/inbox?dummy=a9e40717291db2f1.
Avoid doing this unless necessary, as it can adversely affect performance due to cache pollution. It is better to provide correct cache control headers if you are able.
There is no objection to reusing an XMLHttpRequest object provided that there is no temporal overlap between requests, however you should not do this with the expectation of any significant performance gain: the effort required to create a new object is negligible in comparison to sending the HTTP request and processing the response. Furthermore you should not do this for requests which might otherwise be capable of executing in parallel, because having them share a single XMLHttpRequest instance would be likely to reduce performance.
An example where reuse makes good sense is in a polling loop used to implement HTTP server push. The requests naturally occur in series, so there should be no risk of contention. However, if an XMLHttpRequest object were needed for any other purpose in the same program then it would probably be best to use a separate one.
Internet Explorer lacks native JavaScript support for XMLHttpRequest prior to IE7. It instead provides an ActiveX control named MSXML2.XMLHTTP with a similar API. Microsoft recommend using version 6.0 of this control if it is available, or failing that version 3.0, but not version 4.0 or 5.0. If none of these are available then Microsoft.XMLHTTP can be tried as a last resort. The native JavaScript class should always be the first preference:
function createXMLHttpRequest() { if (window.XMLHttpRequest) { return new XMLHttpRequest(); } try { return new ActiveXObject('MSXML2.XMLHTTP.6.0'); } catch (e) {} try { return new ActiveXObject('MSXML2.XMLHTTP'); } catch (e) {} try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} return null; }
Further points to be aware of:
jQuery provides several methods for performing HTTP requests, the most generic being jQuery.ajax which has broadly similar functionality to a raw XMLHttpRequest object. There are also a number of shorthand methods providing more specialised operations:
jQuery.get | method = 'GET' |
jQuery.post | method = 'POST' |
jQuery.getJSON | method = 'GET'; dataType = 'json' |
jQuery.getScript | method = 'GET'; dataType = 'script'; execute script |
.load | method = 'GET'; dataType = 'html'; load HTML into element |
For example, given an empty
element with an id of inbox:
<div id="inbox"/>
you could populate that element with pre-formatted HTML obtained from the URI /folders/inbox.html using .load:
$("#inbox").load("/folders/inbox.html");
One of the main benefits of using jQuery is to abstract away differences between browsers. The 1.x branch provides support back to IE6.
AngularJS provides a service named $http for making HTTP requests. JSON-encoded responses are automatically detected and decoded. For example, the following code fragment would fetch and decode the webmail inbox content as per the scenario above:
angular.module("webmail", []).controller("messages", function ($scope, $http) { $http({method: "GET", url: "/folders/inbox"}).then(function (response) { $scope.messages = response.data; }); });
The main benefit here is that the fetched data can be declaratively bound to page elements without the need to perform any explicit manipulation of the DOM. For example, to iterate over the inbox content and render it as an HTML table:
<div ng-controller="messages"> <table> <tr ng-repeat="message in messages"> <td>{{message.From}}</td> <td>{{message.Subject}}</td> <td>{{message.Date}}</td> </tr> </table> </div>