What are Angular HTTP interceptors anyway? - codecentric AG Blog

:

Sometimes it takes more time or words to describe a concept in natural language, than to express it in code. Angular’s HTTP interceptors are such an example. The implementation is about half the size (about 30 lines of code at the time of writing) of the documentation (80 lines). Understanding the code behind frameworks’ features is often desirable as it improves understanding of language concepts (Promises in the case) as well as use cases for frameworks’ features.

In this article I am presenting the code that is driving Angular’s HTTP interceptor feature. You will learn how easy it is to look at Angular’s codebase, how versatile Promises are and we will look back at a common cross-cutting concern which is handled with HTTP interceptors.

Interceptors, a short reminder

Angular’s HTTP interceptors can be used to pre- and postprocess HTTP requests. Preprocessing happens before requests are executed. This can be used to change request configurations. Postprocessing happens once responses have been received. Responses can be transformed via postprocessing. Global error handling, authentication, loading animations and many more cross-cutting concerns can be implemented with HTTP interceptors. The following listing shows how a request (preprocessing) and response (postprocessing) interceptor can be used.

const module = angular.module('interceptorTest', []);
 
module.config($httpProvider => {
  $httpProvider.interceptors.push(
    createInterceptor.bind(null, 'A'),
    createInterceptor.bind(null, 'B')
  );
});
 
module.run($http => {
  $http.get('https://api.github.com')
  .then(response =>; console.log('Response handler'));
});
 
angular.bootstrap(document.documentElement, [module.name]);
 
function createInterceptor(id) {
  return {
    request(config) {
      console.log(`Interceptor ${id}: Request`);
      return config;
    },
 
    response(response) {
      console.log(`Interceptor ${id}: Response`);
      return response;
    }
  };
}
 
// Generates the following output:
// Interceptor A: Request
// Interceptor B: Request
// Interceptor B: Response
// Interceptor A: Response
// Response handler

const module = angular.module('interceptorTest', []);module.config($httpProvider => { $httpProvider.interceptors.push( createInterceptor.bind(null, 'A'), createInterceptor.bind(null, 'B') ); });module.run($http => { $http.get('https://api.github.com') .then(response =>; console.log('Response handler')); });angular.bootstrap(document.documentElement, [module.name]);function createInterceptor(id) { return { request(config) { console.log(`Interceptor ${id}: Request`); return config; },response(response) { console.log(`Interceptor ${id}: Response`); return response; } }; }// Generates the following output: // Interceptor A: Request // Interceptor B: Request // Interceptor B: Response // Interceptor A: Response // Response handler

Interceptors are registered with the $httpProvider by adding them to the $httpProvider.interceptors array. Interceptors execute in the order in which they appear in the aforementioned array, during the request phase. For the response phase, they are executed in reverse order. The following image illustrates the process and should help understand the reverse order part.

Request interception process

Looking at the source

Angular makes it easy to inspect the framework’s sources. The framework’s documentation is generated from the sources and every API documentation page contains a reference to the sources. A click on the View Source button in the top right corner sends you right to the source code on GitHub.

The View Source button is located in the top right corner

As mentioned in the introduction, the interceptor source code is fairly short. This is due to the fact that the interceptors are based on Promises. Promises are responsible for the complicated mechanism of config (preprocess) and response (postprocess) transformation orchestration. An example for such a transformation is a rejection handler which falls back to cached values when requests fail. For reference, this is the source code at the time of writing.

// Source: https://github.com/angular/angular.js/blob/3fd48742b0fecbc470c44b465ba90786bda87451/src/ng/http.js#L794-L812
var chain = [serverRequest, undefined];
var promise = $q.when(config);
 
// apply interceptors
forEach(reversedInterceptors, function(interceptor) {
  if (interceptor.request || interceptor.requestError) {
    chain.unshift(interceptor.request, interceptor.requestError);
  }
  if (interceptor.response || interceptor.responseError) {
    chain.push(interceptor.response, interceptor.responseError);
  }
});
 
while (chain.length) {
  var thenFn = chain.shift();
  var rejectFn = chain.shift();
 
  promise = promise.then(thenFn, rejectFn);
}

// Source: https://github.com/angular/angular.js/blob/3fd48742b0fecbc470c44b465ba90786bda87451/src/ng/http.js#L794-L812 var chain = [serverRequest, undefined]; var promise = $q.when(config);// apply interceptors forEach(reversedInterceptors, function(interceptor) { if (interceptor.request || interceptor.requestError) { chain.unshift(interceptor.request, interceptor.requestError); } if (interceptor.response || interceptor.responseError) { chain.push(interceptor.response, interceptor.responseError); } });while (chain.length) { var thenFn = chain.shift(); var rejectFn = chain.shift();promise = promise.then(thenFn, rejectFn); }

As you can see, request interceptors are chained together in the form of Promise transformations via then(onFulfilled, onRejected). This has interesting implications.

  1. Response error interceptors can be used to resolve errors before the caller gets a chance to inspect the request’s result. This means that fallbacks can be implemented in a transparent way.
  2. Because there are request and response interceptors that are capable of transforming config and response objects, the $http request config can be extended with domain specific attributes that are translated to standard HTTP headers.
  3. Generic error handling is hard to get right. While every error could be caught, logged and the user informed, this is often not sufficient from a usability point of view. Some HTTP errors are expected and handled by userland code in Angular services, while others are unexpected and not handled. Angular’s interceptors concept does not provide native means to differentiate between expected and unexpected errors.

Conclusion

Angular’s HTTP interceptors are a fine example for the benefits of reading source code. It also shows the strength of Promises and how versatile they are. At the same time a closer look reveals that we need to put more energy into global error handling. A solution to take care of unhandled rejected Promises would need to exist to implement global error handling properly. The Q Promise library has something like this on the global Q object (API). Angular’s version of Q unfortunately does not contain this feature.