HTTP interceptors are an impressive AngularJS feature that doesn't get nearly enough press. Interceptors define custom transformations for HTTP requests and responses at the application level. In other words, interceptors define general rules for how your application processes HTTP requests and responses.

Sound a little vague? Now you know how I felt scouring the internet for content about interceptors. The AngularJS docs on the subject are light on examples and real use cases. In this article, you'll learn interceptors by example.

Response Interceptors

The most basic use case for interceptors is handling REST API enveloping. Suppose you have a REST API that, up until recently, returned the HTTP status as part of the response body. The Instagram API is a good example.

{
  "meta": {
    "code": 200
  },
  "data": {
    "name": "Val"
  }
}

Suppose the API maintainer decides returning the HTTP status code in the body is no longer necessary. The new API response would look like this:

{
  "name": "Val"
}

One approach would be to change every place where you use meta.code. Unfortunately, in a big application, that could be a tedious task. Enter a simple HTTP interceptor:

var m = angular.module('myApp', []);

m.config(function($httpProvider) {
  $httpProvider.interceptors.push(function() {
    return {
      response: function(res) {
        /* This is the code that transforms the response. `res.data` is the
         * response body */
        res.data = { data: data };
        res.data.meta = { status: res.status };
        return res;
      }
    };
  });
});

This simple HTTP interceptor demonstrates the basic interceptor syntax. Interceptors are represented as an array of functions on the $httpProvider provider. Providers can only be accessed in config() blocks, so you must define your interceptors in a call to app.config(). The interceptor function must return a JSON map which can contain any of the following 4 keys:

  • request
  • response
  • requestError
  • responseError

As you saw in the previous example, the response property should be a function that takes a single parameter, the res object, and returns a response object. The above example modifies the res object and returns it, but you can return a completely new object if you need to. Here's what the JSON.stringify() output looks like for a sample HTTP request:

{
  "data": {
    "success": true
  },
  "status": 200,
  "config": {
    "method": "GET",
    "transformRequest": [
      null
    ],
    "transformResponse": [
      null
    ],
    "url": "/sample",
    "headers": {
      "Accept": "application/json, text/plain, */*"
    }
  },
  "statusText": "OK"
}

Your response function can modify this object in any way it sees fit. In this particular example, the underlying API changed its output format. However, the response interceptor abstracts out the API change so your code can continue to use response.data.meta.status indefinitely.

This status example is useful for educational purposes, but you're unlikely to see it in the real world. The primary use case for interceptors in practice involves request interceptors and authentication. You'll investigate this use case in the next two sections.

Request Interceptors: Setting the Authorization Header

Suppose you track the currently logged in user with a service called userService. This userService contains session credentials that you want to send to the server with every HTTP request. In this example, you will attach these credentials in the request Authorization header. This is a heavily simplified version of the HTTP Basic Access Authentication protocol.

Suppose userService looks like this:

var m = angular.module('myApp', []);

m.factory('userService', function() {
  return {
    getAuthorization: function() {
      return 'Taco';
    }
  }
});

How are you going to tie this service into an interceptor? It's easier than you think: the interceptor function is tied in to the AngularJS dependency injector.

m.config(function($httpProvider) {
  // Pull in `userService` from the dependency injector
  $httpProvider.interceptors.push(function(userService) {
    return {
      request: function(req) {
        // Set the `Authorization` header for every outgoing HTTP request
        req.headers.Authorization =
          userService.getAuthorization();
        return req;
      }
    };
  }
});

Thanks to this request interceptor, all your outgoing HTTP requests now have an Authorization header. Request interceptors are analagous to response interceptors: they take a request object and return a request object, which may or may not be the original req parameter. Below is the JSON.stringify() output from a sample req object.

{
  "method": "GET",
  "transformRequest": [
    null
  ],
  "transformResponse": [
    null
  ],
  "url": "/sample",
  "headers": {
    "Accept": "application/json, text/plain, */*"
  }
}

The ability to set the Authorization header on every request is useful. But, what happens if a user's session times out while they're on your page? Your server will start returning HTTP 401's, and your user's requests will start failing. You could just redirect the user to a login page, but then the original request gets lost. The last section, about error interceptors, will show you how to handle this case gracefully.

Error Interceptors: Fun with Promises

The goal of this section is to handle session timeouts gracefully. Suppose your server returns an HTTP 401 ("Unauthorized"). With error interceptors, you can prompt the user to log in and retry the original HTTP request once they've entered in their credentials!

Error interceptors make heavy use of promises. If you're unfamiliar with promises, here is a reasonable introduction. Note that interceptors assume ES6 style promises, relying primarily on the .then() function. Thus most error interceptors will rely on AngularJS' $q service, which is a port of the popular promises library q. In theory, you can use promises libraries like q or Bluebird with error interceptors. You can even use promises returned by the $http service, because $http promises have a .then() function. For instance,

$http.get('/sample').
  success(function(data, status, headers, config) {
  });

is equivalent to

$http.get('/sample').
  then(function(res) {
    // Use res.data, res.status, res.headers, res.config
  });

So what does a responseError interceptor look like? Below is the interceptor that will handle HTTP 401's.

m.config(function($httpProvider) {
  $httpProvider.interceptors.push(function($q, $injector, userService) {
    return {
      request: function(request) {
        request.headers.authorization =
          userService.getAuthorization();
        return request;
      },
      // This is the responseError interceptor
      responseError: function(rejection) {
        if (rejection.status === 401) {
          // Return a new promise
          return userService.authenticate().then(function() {
            return $injector.get('$http')(rejection.config);
          });
        }

        /* If not a 401, do nothing with this error.
         * This is necessary to make a `responseError`
         * interceptor a no-op. */
        return $q.reject(rejection);
      }
    };
  });
});

The above code assumes the userService.authenticate() function returns a then()-able promise around the user entering their password. If that condition is met, the above interceptor is sufficient to gracefully handle session timeouts. If the server returns an HTTP 401, this interceptor will prompt the user to log in and return a new promise. This new promise wraps the user logging in and the $injector.get('$http')(rejection.config) call, which is responsible for retrying the HTTP request. Thanks to the magic of promises, the returned promise resolves if and only if the user authenticates and the retried HTTP call succeeds.

The above example will continue to retry so long as the HTTP request fails. This is because HTTP interceptors are triggered on every HTTP request, including HTTP requests triggered by interceptors. If the user enters an incorrect password, you can still resolve the userService.authenticate() promise. The HTTP interceptor will then ask the user to authenticate again and continue to retry the HTTP request!

All that's left is to implement a suitable userService.authenticate() function. Thankfully, this is a simple task with Angular-UI Bootstrap's $modal service. To open a modal with this service, you call $modal.open(). The return value of $modal.open() has a result property that wraps the "submit the modal" operation in a promise. Thus, you can implement the userService.authenticate() function as shown below.

var authenticate = function() {
  var $modal = $injector.get('$modal');

  var modal = $modal.open({
    template: '<div style="padding: 15px">' +
              '  <input type="password" ng-model="pwd">' +
              '  <button ng-click="submit(pwd)">' +
              '    Submit' +
              '  </button>' +
              '</div>',
    controller: function($scope, $modalInstance) {
      $scope.submit = function(pwd) {
        $modalInstance.close(pwd);
      };
    }
  });

  /* `modal.result` is a promise that gets resolved when
   * $modalInstance.close() is called */
  return modal.result.then(function(pwd) {
    password = pwd;
  });
};

That's it! Now that you have a promise wrapper around the user logging in, your error interceptor can handle HTTP 401's gracefully. In practice, your authenticate() function will likely make an HTTP request to the server to get a new session. But, this example is sufficient to demonstrate how you would make the HTTP interceptor portion work. Happy intercepting!

If you liked this article, check out my upcoming book, Professional AngularJS. The book contains a thorough overview of HTTP in AngularJS, as well as guides for integrating with web sockets and Firebase.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus