As Dojo moves toward its 2.0 release, our focus has been on giving developers tools that will help them be productive in any JavaScript environment. This means creating consistent APIs across all environments. One area that has been sorely lacking, in this regard, is Dojo’s IO functions. We’ve always provided developers with a way to make requests in the browser (dojo.xhr*, dojo.io.iframe, dojo.io.script), but the API has been less consistent than some of us would like (dojo.xhrGet, dojo.io.script.get, etc.). Additionally, we’ve never provided a server-side implementation, and if we had, it would have been another module name and API call to remember.

With the release of Dojo 1.8, we have introduced the dojo/request API which provides consistent API calls between browsers, request methods, and environments:

require(["dojo/request"], function(request){
    var promise = request(url, options);
    promise.then(
        function(data){
        },
        function(error){
        }
    );
    promise.response.then(
        function(response){
        },
        function(error){
        }
    );
    request.get(url, options).then(...);
    request.post(url, options).then(...);
    request.put(url, options).then(...);
    request.del(url, options).then(...);
});

The function signature for dojo/request and all providers is a URL and an object specifying options for the request. This means using dojo/request is as easy as passing it a string argument; the options argument is truly optional. Let’s take a look at the common properties you can pass on the options object:

  • method – HTTP method to use for the request (default is 'GET'; ignored by dojo/request/script)
  • querykey=value string or { key: 'value' } object specifying query parameters
  • data – string or object (serialized to a string with dojo/io-query.objectToQuery) specifying data to transfer (ignored by GET and DELETE requests)
  • handleAs – string specifying how to handle the server response; default is ‘text’, other possibilities include ‘json’, ‘javascript’, and ‘xml’
  • headers{ 'Header-Name': 'value' } object specifying headers to use for the request
  • timeout – integer specifying how many milliseconds to wait before considering the request timed out, canceling the request, and rejecting the promise

The consistency of the API also extends to its return value: all dojo/request methods return a promise that will resolve to the data contained in the response. If a content handler was specified when the request was made (via the handleAs option), the promise will resolve to the result of the content handler; otherwise it will resolve to the response body text.

Promises returned from dojo/request calls extend normal dojo/promise behavior with an additional property: response. This property is also a promise that will resolve to a frozen object (where available) describing the response in more detail:

  • url – final URL used to make the request (with query string appended)
  • options – options object used to make the request
  • text – string representation of the data in the response
  • data – the handled data in the response (if handleAs was specified)
  • getHeader(headerName) – a function to get a header from the request; if a provider doesn’t provide header information, this function will return null.

The example at the top of this post shows this in action through the use of promise.response.then

Providers

Behind the scenes, dojo/request uses providers to make requests. For each platform, a sensible default is chosen: browsers will use dojo/request/xhr and Node will use dojo/request/node. It should be noted that newer browsers (IE9+, FF3.5+, Chrome 7+, Safari 4+) will use the new XMLHttpRequest2 events instead of XMLHttpRequest‘s onreadystatechange that is used in older browsers. Also, the Node provider uses the http and https modules, which means no XMLHttpRequest shim needs to be employed on the server.

If a provider other than the default needs to be used (for instance, the provider for JSON-P), there are three choices available: use the non-default provider directly, configure it as the default provider, or configure the request registry.

Because all providers conform to the dojo/request API, non-default providers can be used directly. The approach taken with the dojo/request API is analogous to the approach of dojo/store. This means that if you only have a few services that return JSON-P, you can use dojo/request/script for those services without having to change the basic API signature. Using a provider this way is slightly less flexible than the other two choices (especially for testing), but is a completely valid way to use a non-default provider.

Another way to use a non-default provider is to configure it as the default provider. This is helpful if we knew that our application was only going to use one provider that wasn’t the default. Configuring the default provider is as simple as setting a provider’s module ID as the requestProvider property of dojoConfig:

<script>
    var dojoConfig = {
        requestProvider: "dojo/request/script"
    };
</script>
<script src="path/to/dojo/dojo.js"></script>

requestProvider can also be set up via data-dojo-config like any other configuration parameter. In addition, any function that conforms to the dojo/request API can be used as the default provider. This means we could develop a custom module that wraps dojo/request/xhr, adds additional headers for authentication, and configure it as our application’s default provider. During testing, a separate provider could be used to simulate responses from the server to test if our application is making requests to the correct services.

Although configuring the default provider gives us more flexibility than using providers directly, it still doesn’t give us the flexibility needed to use one API call (dojo/request) with different providers based on a specified criteria. Let’s say our application’s data services needed one set of authentication headers for one service and an entirely different set of headers for a second service. Or JSON-P for one and XMLHttpRequest for another. This is where using dojo/request/registry shines.

Registry

One module that has been present in DojoX for a long time, but is not widely used is dojox/io/xhrPlugins. This module provides a way to use dojo.xhr* as the interface for all requests, whether those requests needed to be made via JSONP, iframe, or another user-defined provider. Because of its usefulness, the idea has been adapted as dojo/request/registry.

dojo/request/registry conforms to the dojo/request API (so it can be used as a provider) with the addition of the register function:

// provider will be used when the URL of a request
// matches "some/url" exactly
registry.register("some/url", provider);

// provider will be used when the beginning of the URL
// of a request matches "some/url"
registry.register(/^some\/url/, provider);

// provider will be used when the HTTP method of
// the request is "GET"
registry.register(
    function(url, options){
        return options.method === "GET";
    },
    provider
);

// provider will be used if no other criteria are
// matched (a fallback provider)
registry.register(provider);

If no criteria are matched and a fallback provider hasn’t been configured, the default provider for the environment will be used. Since dojo/request/registry conforms to the dojo/request API, it can be used as the default provider:

<script>
    var dojoConfig = {
        requestProvider: "dojo/request/registry"
    };
</script>
<script src="path/to/dojo/dojo.js"></script>
<script>
    require(["dojo/request", "dojo/request/script"],
        function(request, script){
            request.register(/^\/jsonp\//, script);
            ...
        }
    );
</script>

This is great if we want to use the platform’s default provider (XHR for browsers) as our fallback. We could also set up a fallback provider using the last API call above, but no other providers could be registered afterward. Instead, dojo/request/registry can be used as a plugin from requestProvider to set the fallback provider:

<script>
    var dojoConfig = {
        requestProvider: "dojo/request/registry!my/authProvider"
    };
</script>
<script src="path/to/dojo/dojo.js"></script>
<script>
    require(["dojo/request", "dojo/request/script"],
        function(request, script){
            request.register(/^\/jsonp\//, script);
            ...
        }
    );
</script>

Now, any request not matching the criteria we have set up will use the module located at my/authProvider.

The power of the registry may not be readily apparent. Let’s take a look at some scenarios which make the benefits stand out. First, let’s consider an application where the server API is in flux. We know the end-points, but we don’t know what headers will be required or even what JSON objects will be returned. We could easily set up a registry provider for each service, temporarily, and start coding the user interface. Let’s say we guess that /service1 will return items as JSON in an items property and /service2 will return them as JSON in a data property:

request.register(/^\/service1\//, function(url, options){
    var promise = xhr(url, lang.delegate(options, { handleAs: "json" })),
         dataPromise = promise.then(function(data){
            return data.items;
        });
    return lang.delegate(dataPromise, {
        response: promise.response
    });
});
request.register(/^\/service2\//, function(url, options){
    var promise = xhr(url, lang.delegate(options, { handleAs: "json" })),
         dataPromise = promise.then(function(data){
            return data.data;
        });
    return lang.delegate(dataPromise, {
        response: promise.response
    });
});

All service requests in the user interface can now be used in the form request(url, options).then(...) and they will receive the proper data. As development proceeds, however, the server team decides that /service1 will return its items as JSON in a data property and /service2 will return its items as XML. Without using the registry, this would have been a major code change; but by using the registry, we’ve decoupled what our widgets and data stores are expecting from what the services are providing. This means the server team’s decisions only cause code to change in two places: our providers. Potentially, we could even decouple our user interface from URLs altogether and use generic URLs which the registry then maps to the correct provider that uses the correct server end-point. This allows end-points to change without the pain of updating code in multiple places.

This decoupling can also be extended to testing. Usually during unit tests, a remote server is not desirable: data can change and the remote server could be down. This is why testing against static data is recommended. But if our widgets and user interface have end-points or request calls hard-coded into them, how do we test them? If we’re using dojo/request/registry we simply register a provider for an end-point that will return static data for requests for our unit tests. No API calls need to change. No code within our application needs to be rewritten.

Conclusion

As you can see, dojo/request was written with the developer in mind: a simple API for simple situations, but flexible options for the most complex applications.

Resources

To learn even more about dojo/request, check out the following resources: