次の方法で共有


Using service filters with the Mobile Services JavaScript SDK

A few weeks ago I answered a question on StackOverflow where the developer needed a way to intercept every request which went to the service (in their case, to display a progress bar while the request was “in flight”). The answer for that was to use a service filter in the mobile services client, which would be called every time a new request was sent. I realized then that the documentation for that feature wasn’t very good: a small description on the MobileServicesClient reference, and a small mention in the “how to use a HTML/JavaScript client” doc which didn’t cover all of the feature). Unlike typed languages, where you could guess based on the parameter types what you should pass and receive from the function (like in the Android version, for example), JavaScript doesn’t have that type information, so it’s harder to find out what we should pass when using it. So here’s this post to try to add more information on this nice feature.

So what is a filter anyway? Filters, used in the Android, iOS and JavaScript client SDKs for mobile services is equivalent to the HttpMessageHandler objects which we can pass to the MobileServiceClient constructor in the .NET runtime – a way to intercept the HTTP requests before they go in the wire, and the corresponding responses before they’re sent to the SDK itself. You can chain multiple filters together, and each of them can look at the request, pass it along to the rest of the chain until the end (at which point it would be sent to the wire), then look at the response. It can also bypass the rest of the chain entirely (which would cause the request not being sent over the wire), which has some nice applications for testing. It can even execute multiple requests to the server for a single request from the client object. So let’s look at some scenarios which show how the filters can be used.

Identity (logging) filter

This is the basic filter which doesn’t do anything with the request nor with the response. The function of the filter is to simply pass the request along to the next filter (or the final point in the chain where it would send the request over the wire). The next function takes two parameters, the request and the function which will be called when the response is available (or an error has occurred). At that point we should call the callback function passed to the filter. Notice that since this is JavaScript, and this filter doesn’t do anything with the error or the response, we can simply pass the callback parameter directly to the next function below.

  1. function identityFilter(request, next, callback) {
  2.     next(request, function (error, response) {
  3.         callback(error, response);
  4.     });
  5. }
  6.  
  7. function simplerIdentityFilter(request, next, callback) {
  8.     next(request, callback);
  9. }

A more interesting usage for such a filter which doesn’t change any of the request or response is in a logging scenario. In the code below, before sending the request to the next element in the chain the filter saves the request method and URI, and when it receives the response it logs it with the values captured before. One thing to notice here is that if the HTTP response is not a successful one (i.e., status code 2xx), the ‘error’ parameter passed to the callback will still be null. Filters act at the request / response level, without looking at their semantics at this point.

  1. function loggingFilter(request, next, callback) {
  2.     var reqMethod = request.type;
  3.     var reqUri = request.url;
  4.     next(request, function (error, response) {
  5.         if (error) {
  6.             log(reqMethod + ' ' + reqUri + ': ' + JSON.stringify(error));
  7.         } else {
  8.             log(reqMethod + ' ' + reqUri + ': ' + response.status);
  9.         }
  10.         callback(error, response);
  11.     });
  12. }

One more bit of information: the request object will typically contain four properties: ‘type’ (the HTTP method), ‘url’ (the request URL), ‘data’ (the request body, as a string) and ‘headers’ (the HTTP headers which will be sent). The response object is basically the XMLHttpRequest object (both in the WinStore and in the HTML/JS versions) which is used to make the call. That object contains properties / methods which can be accessed to retrieve the response status code, headers and body.

Bypassing the network

Each filter implementation receives a function ‘next’ which the filter can call to pass the request along, but it doesn’t really need to do that. It can invoke the callback directly, which will effectively bypass the network and send a response to the client directly. One usage of this which I’ve seen and used in other projects (not with Azure Mobile Services, though, at least not yet) is to simulate occasional failures to make sure that the client handles them correctly. I first saw this idea in a Jeff Atwood’s post about what he called the “chaos monkey”, which would insert random failures at different points in a system (apparently this originated from the Netflix engineering team, to explain how they dealt with AWS failures). This is a good scenario where a filter would work – if we add such a filter to our client, we can make it randomly return errors, and this will be applied to all operations (tables and APIs) initiated from the client.

Below is a possible implementation of the chaos monkey filter. Notice that before I mentioned that the response object is the XMLHttpRequest object. But since we’re using JavaScript, we don’t need to create such object, instead letting using duck typing to create a response object which looks like that one. By looking at the source for the mobile services JavaScript SDK, I only saw three members of the object being used: ‘status’, ‘responseText’ and the ‘getResponseHeader’ method. So this is all we need to implement in our response object, and we have our chaos monkey filter:

  1. var errorProbability = 0.01;
  2. function chaosMonkeyFilter(request, next, callback) {
  3.     if (Math.random() < errorProbability) {
  4.         var response = {
  5.             status: 500,
  6.             getResponseHeader: function (headerName) {
  7.                 return (headerName === 'Content-Type') ? 'application/json' : null;
  8.             },
  9.             responseText: JSON.stringify({ error: 'The chaos monkey attacks again!' })
  10.         };
  11.         callback(null, response);
  12.     } else {
  13.         next(request, callback);
  14.     }
  15. }

There are other scenarios where bypassing the networking would be a good idea, such as mocking the network responses even for “positive” cases, where you want to test the client in isolation from the service, and can “play” the responses from any custom scripts you have in the server.

Caching authentication tokens

Another scenario in which a filter is useful is when we’re caching authentication tokens after a successful login. When a mobile service client logs in to a service, the authentication token which it receives in response to the login operation is valid for a certain number of days (30, if I remember correctly). That means that we can reuse the same token for some time, without having the user log in again (if you need to access the authentication provider API, it’s possible that the credentials stored inside the mobile services token will expire, requiring another login, but I won’t go into the details here).

A filter is a good candidate for implementing this logic. Since it applies to all requests from the client, whenever it gets a 401 response, it can try to login again and then retry the original request, adding the correct authentication header so that the second time the authentication should succeed.

  1. var tokenCacheFileName = 'tokenCache.txt';
  2. function createTokenCachingFilter(client, provider) {
  3.     return function reloginFilter(request, next, callback) {
  4.         next(request, function (error, response) {
  5.             if (error || response.status !== 401) {
  6.                 // If there is an error, or the response is not
  7.                 // 'unauthorized', return that
  8.                 callback(error, response);
  9.                 return;
  10.             }
  11.  
  12.             client.login(provider).done(function (user) {
  13.                 // if the login succeeded, save the auth token
  14.                 saveAuthToken(user).done(function () {
  15.                     // Add the appropriate header to the request
  16.                     request.headers['X-ZUMO-AUTH'] = user.mobileServiceAuthenticationToken;
  17.                     // then try the request again
  18.                     next(request, callback);
  19.                 });
  20.             }, function (err) {
  21.                 // error logging in, will return original response
  22.                 callback(error, response);
  23.             });
  24.         });
  25.     };
  26.  
  27.     function saveAuthToken(user) {
  28.         var toSave = { id: user.userId, token: user.mobileServiceAuthenticationToken };
  29.         return WinJS.Application.local.writeText(tokenCacheFileName, JSON.stringify(toSave));
  30.     }
  31. }
  32. function loadAuthToken(client) {
  33.     var app = WinJS.Application;
  34.     return app.local.readText(tokenCacheFileName, null).then(function (contents) {
  35.         if (contents) {
  36.             var user = JSON.parse(contents);
  37.             client.currentUser = { userId: user.id, mobileServiceAuthenticationToken: user.token };
  38.             return client.currentUser;
  39.         } else {
  40.             return null;
  41.         }
  42.     });
  43. }

Just a big warning before anyone thinks about using this code: the mobile services token is stored in the client unencrypted. This is bad for obvious reasons. I used this for simplicity sake of this scenario (I wanted to focus on the revalidation itself), but for real applications you should definitely use some secure storage, such as the Credential Locker for Windows Store / JS applications.

Wrapping up

Filters aren’t something most people will deal with when creating mobile applications with JavaScript that talk to an Azure Mobile Service. But if the scenario comes up, hopefully this post will help understanding how to use that feature.