Angular HTTP Interceptors

Introduction

Recently I was working on a project that required both the addition of data to outgoing calls and the manipulation of successful responses. HTTP Interceptors to the rescue! This feature of Angular allows the interception of an HTTP call at the four significant points of its lifecycle: request, requestError, response and responseError and is probably best illustrated with a demonstration of each use case.

Request Interceptor

Intercepting the request part of an HTTP call is useful for adding modifying or removing data prior to sending, or blocking its execution completely. Additions and manipulations can be both for values that are sent up to the server, such as appending an extra header containing a token value, or setting a value on the config that needs to be retrieved after the call is complete. Blocks on execution can be vital for calls where required values are missing such as auth tokens, or other calls/system events are pending and access to particular endpoints should be restricted.

.factory('ConfigAppendInterceptor', function(){

  var request = function(config) {
    console.log('request');
    config.headers.myHeaderValue = 'abc123';
    config.valueStoredAtTimeOfCall = 'def456';
    // Note that either an object or a promise can be returned here
    return config;
  }

  return {
   request: request
  }

})

.factory('LoginBlockInterceptor', function($q, UserService){

  var request = function(config) {
    var defer = $q.defer();
    if(!UserService.isLoggedIn() && config.url === '/api/user') {
      defer.reject();
    } else {
      defer.resolve('userIsNotLoggedIn');
    }
    return config;
  }

  return {
    request: request
  }

})

.config(function($httpProvider) {
  $httpProvider.interceptors.push('ConfigAppendInterceptor');
  $httpProvider.interceptors.push('LoginBlockInterceptor');
})

Request Error Interceptor

A request error interceptor helps when a request interceptor has blocked the HTTP call. This example builds on the LoginBlockInterceptor above by attemping to log the user in through an async process, which if successful will resolve the promise.

.factory('LoginForceInterceptor', function($q, UserService){

  // Useful for setting values pre sending to server
  var requestError = function(rejectReason) {
    var defer = $q.defer();

    if(rejectReason === 'userNotLoggedIn') {
      // Do some asynchronous stuff and if successful return resolve
      // the promise
      // defer.resolve(config)
      // or if unsuccessful
      // defer.reject(rejectReason)
    } else {
      defer.reject(rejectReason);
    }

    return defer.promise;
  }

  return {
    requestError: requestError
  }

})

.config(function($httpProvider) {
  $httpProvider.interceptors.push('LoginForceInterceptor');
})

Response Interceptor

A response interceptor is used to modify the downloaded data aiding amongst other things, its decoration with extra information or crosslinked data from elsewhere in the application

.factory('ResponseManipulatorInterceptor', function(SpaceShipService){
  var response = function(response) {
    var user = response.data.user;
    if(user) {
      user.downloadTimetamp = (new Date()).getTime();
      user.spaceShip = SpaceShipService.getSpaceShipById(user.spaceShipId);
    }
  }

  return {
    response: response
  }
})

.config(function($httpProvider) {
  $httpProvider.interceptors.push('ResponseManipulatorInterceptor');
})

Response Error Interceptor

Intercepting response errors is useful in situations where one call has failed and the system wants to gracefully cope with the fallout, such as a session expiring. In this instance a response error interceptor would catch the fact that the call has failed, then through an asynchronous process, possibly involving some user interaction, such as popping up a login window that triggers a re-sign in call refreshing the session, then reactivate the initial http call through the response.config property.

.factory('SesssionReactivatorInterceptor', function($q){
  var responseError = function(response){
    if(response.status == 419) {
      var defer = $q.defer();
      // Do some async stuff then call defer.resolve(response.config) passing
      // the original config through or defer.reject if the logging back in
      // was not successful

      return defer.promise;
    } else {
      return $q.reject(response);
    }
  }

  return {
    responseError: responseError
  }
})

.config(function($httpProvider){
  $httpProvider.interceptors.push('SesssionReactivatorInterceptor');
})

It should be noted here that although it isn't specified in the documentation, Angular seems to have issues if both request and requestError methods or response and responseError methods are defined in the same interceptor. So for safety's sake I've defined each of the handler methods in their own interceptor.

Conclusion

HTTP interceptors provide a way to catch the results of both successful and unsuccessful calls to remote endpoints. They facilitate both the manipulation of downloaded data, and elegant recoveries from problematic calls, which both serve nicely to hide complexity from parts of the application that should remain naive to the minutiae of how those calls happen. They're not the kind of thing you'll use on every project, but for very specify problems they're a real life saver.