Looking to unit test your interceptors? Read the short and practical introduction How to Test Angular Response Interceptors or Testing Angular Request Interceptors.
I recently got to work more with Angular interceptors. They are a really useful and versatile tool to have in your toolbox. Interceptors can be used to add headers to requests for authentication, alter outgoing and incoming data, mock api responses, provide error handling and retry logic, time the delay between request and response, the list goes on.
There's one use case in particular that got me thinking: the ability to change incoming and outgoing data. Sometimes it's necessary to convert data when talking to an API. Imagine, for example, an API that isn't fully under our control. Or one where changes may be costly. Or one where our application is one of many consumers, each with conflicting demands. The need to adapt data is quite common. Interceptors are a great way to achieve this, and can do so while keeping the rest of our application clean.
We'll start with the basics and look at what interceptors are and how we can set them up, before coming back to think about how interceptors improve our application architecture.
Interceptors are defined in the Angular HTTP module. An Interceptor is a service that's injected into the HttpClientModule to act as middleware for HTTP requests. Any request emitted from the HttpClient travels through a chain of interceptors, any or all of which may change the request as it passes through.
The implementation follows a pattern the Gang of Four dubbed 'Chain of Responsibility'. The pattern is meant to decouple the sender of a request from the receiver. In between sender and receiver is a chain of objects ('handlers'). Each handler has the option to handle the request itself or pass it on to the next handler in the chain. This requires all handlers to implement a common interface that provides exactly those two things: handle the request, pass it on to a successor.
Let's see this pattern at work. When we go to the source code, we first see the HttpInterceptor
interface. It has one method named intercept
. The method gives us access to the HTTP request as it passes through, so we can modify it.
Take a look at a simple no-op interceptor generated from the Interceptor schematic.
1 2 3 4 5 6 7 8 9 10 11 | @Injectable() export class MyNoopInterceptor implements HttpInterceptor { constructor() {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { return next.handle(request); } } |
As we can see, the intercept method also takes a second argument, called next. Note that this argument is typed as an HttpHandler
, not an HttpInterceptor. Since we're building a chain of interceptors, we might have expected the parameter type to be HttpInterceptor. But we already saw in our description of the Chain of Responsibility pattern that the objects in the chain must have a common Handler
interface. Every handler keeps a reference to the next handler in the chain, and knows just enough about the service it's wrapping to pass it the request and return the result. And that's exactly the role HttpHandler
plays. The interface consists of a single method, called handle
. And a reference to its successor is provided in its constructor.
It's a lot of umph in surprisingly few lines of code. Look at the way the HttpInterceptorHandler
wraps interceptors, for example. The implementation can be found right next to the interceptor definition. Its handle method is just one line. Guess what? It calls the intercept method on the wrapped interceptor (providing it with the reference to the next handler).
1 2 3 4 5 6 7 8 9 10 | /** * `HttpHandler` which applies an `HttpInterceptor` to an `HttpRequest`. */ export class HttpInterceptorHandler implements HttpHandler { constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {} handle(req: HttpRequest<any>): Observable<HttpEvent<any>> { return this.interceptor.intercept(req, this.next); } } |
While that covers the theory and interfaces, we haven't quite put the finger on how this transformation from interceptor to handler takes place. How is the chain formed? To answer that we must turn to the HttpClientModule
.
Interceptors must be registered with a module that's also importing the HttpClientModule. The Angular docs helpfully remind us that Interceptors we register will be added to that instance of the HttpClientModule, and fed requests made by that instance of the HttpClient. Location matters. It also says that rather than provide the interceptor directly, we should add it to the HTTP_INTERCEPTORS token. The multi
property tells the injector to add classes provided for that token to a collection. This allows us to build an array of interceptors, injected in the order in which they were registered.
1 2 3 4 5 6 7 8 9 10 11 | @NgModule({ ... providers: [ { provide: HTTP_INTERCEPTORS, useClass: MyNoopInterceptor, multi: true } ], }) export class AppModule { } |
So we've registered an interceptor to be added to a chain of interceptors. But again, what actually gets added is a chain of HttpHandlers. When does this happen? The HttpClientModule creates an HttpInterceptingHandler
at compile time - another HttpHandler. The handler receives the HTTP_INTERCEPTOR tokens we provided, and wraps those in a chain of InterceptorHandlers. Being a good handler itself, it keeps a reference to the first handler in the chain that was just created. The HttpInterceptingHandler
goes through this process (once) at runtime, to allow our interceptors to get 'lazy loaded' and avoid circular dependencies.
When HttpClient
makes a request, it doesn't call any of our interceptors directly, nor does it keep track of the chain of interceptors. It calls the HttpInterceptingHandler
, which calls the next handler, which calls the next handler, which...
This is the Chain of Responsibility pattern at its finest. The HttpClient doesn't know how many handlers there are, or what they will do to the request. And when we're writing our interceptors we also don't have to build and manage a chain manually. That chain can provide for itself. By the power of the handler abstraction.
One last thing. So far we've been talking about a chain of handlers 'in between' a sender and an implicit receiver. But lo and behold the HttpBackend gets wrapped in a handler in exactly the same way. Why not? It's just another 'something' acting on the request. This is brilliant. Maybe some interceptor decides to completely rewrite the request. Or maybe it cuts the chain short and returns a response. Maybe the HttpBackend is a real backend, maybe it's a fake. Who knows. The HttpClient sure doesn't. It's handlers all the way down.
It's handlers all the way down.
Go ahead and give yourself five points for Gryffindor if you're with us so far. The takeaway from all of this is that when we register an interceptor it's transformed into an HttpHandler, and added in a chain between the HttpClient and HttpBackend. Requests travel from each handler in the chain to the next.
Let's implement an interceptor to a Very Serious Applicationtm: the Rock-Paper-Scissors-Lizard-Spock Training Simulator.
When a user clicks one of the buttons a request is made to api.toys/rock_paper_scissors_lizard_spock and the play is resolved. Unfortunately, the api.toys data transfer object (Dto) doesn't match the domain model of our application. When we say hero, it says player. When we say villain it says cpu. What it calls move we call result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | export class Round { num?: number; villain: Move; hero: Move; result: string; winner: Winner; } export type Move = 'rock' | 'paper' | 'scissors' | 'lizard' | 'spock'; export type Winner = 'hero' | 'villain' | 'draw'; export class RoundDto { player: string; cpu: string; result: string; winner: string; move: string; } |
What we need to do is adapt a request to resolve a RoundDto to an instance of Round. We can generate an interceptor with the schematic we saw before: ng generate interceptor round-adapter
. We called it RoundAdapter to name it after what it does rather than what it is - but this is a matter of taste. Don't forget to register the interceptor with the module that's also importing HttpClientModule. In our case, that's simply the root module.
After registering the interceptor, it should dutifully intercept every request. But it's still a no-op, and will pass along every request unaltered. Let's change that!
First, we want to intercept a specific method and url. We can write this logic directly in the intercept method, but it looks a little cleaner if we define an shouldIntercept
method. Note that in either case we pass the request to the next HttpHandler. If we don't pass it on the request never reaches the backend. Of course, if the request is one we want to intercept, we get to change it along the way.
Second, we need to alter the request. The change we make depends on the use case. If we were interested in changing the request body, we could create a new request with a different body and pass it to next.handle
. In this case, we want to add behaviour when the API responds. We can use RxJS operators to achieve that: next.handle
returns a stream of HttpEvents. That means any RxJS operators we add will be executed whenever an HttpEvent is emitted. In our case, we use the map
operator to map the response through a method called roundAdapter
. The intercept method should remain small and declarative.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | @Injectable() export class RoundAdapter implements HttpInterceptor { private readonly interceptedUrl = environment.endpoint + '/api/rock_paper_scissors_lizard_spock'; constructor() {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { if (this.shouldIntercept(request)) { return next.handle(request).pipe( map(this.roundAdapter) // NB. equivalent to // map(value => this.roundAdapter(value)) ); } return next.handle(request); } private shouldIntercept(request): boolean { return request.method === 'GET' && request.url === this.interceptedUrl; } private roundAdapter(response) { function getWinner(winner): Winner { return winner === 'player' ? 'hero' : winner === 'cpu' ? 'villain' : 'draw'; } if (response instanceof HttpResponse) { return response.clone({ body: { hero: response.body.player, villain: response.body.cpu, winner: getWinner(response.body.winner), result: response.body.move } as Round }); } return response; } } |
The details of the roundAdapter
method can be more involved, and are specific to the application at hand. Two things to note: first, the method checks if the response is an instance of HttpResponse; this is basic defensive programming - there may be other HttpEvents that our function wouldn't know what to do with. Second, when we adapt the response we clone it and provide a new response body. The clone method is required because both request and response objects should be treated as immutable. The clone method gives us a new request that's identical to the original - except for the keys provided.
That's it! We now have an interceptor that translates the API's Dto to a model we can work with.
Let's return to the bigger picture. We've now created an interceptor and applied it to a specific url to adapt response data. But why don't we just do this conversion in our API service? Shouldn't the responsibility of the API service be to, well, handle calling the API?
To answer this question, let's look at an implementation of the API service, with the interceptor in place. The playRound
method makes a get call to the intercepted route. Note it expects an Observable of type Round, while the API doesn't actually provide that at all. But as long as the interceptor works, this will always be true!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @Injectable({ providedIn: 'root' }) export class ApiService { private readonly headers: HttpHeaders = new HttpHeaders({ 'Content-Type': 'text/plain' }); private readonly url = environment.endpoint + '/api/rock_paper_scissors_lizard_spock'; constructor(private http: HttpClient) { } playRound(move: Move): Observable<Round> { return this.http.get<Round>(this.url, { headers: this.headers, params: { guess: move } }); } } |
By hiding implementation details like domain model adapters, the service is easier to reason about and maintain. Of course this is just a toy example, and real world applications tend to be a lot more complicated; error handling, retry logic, request headers, and so on. Some complexity will likely find its way into an API service anyway - and that's okay. I'd argue it's exactly because services tend to grow more complex that the ability to hide implementation details one layer down is so valuable.
In any case, there's no hard right or wrong. The guiding principle for complexity is not one of place, but of direction. We should aim to push complexity out to the edges of our application. In this case, the fewer parts of our application have to deal with different models, the less defensive code we need to keep our application from exploding. This is generally true of any input that's not under our direct control. User input, for example, should be sanitized in a similar way. Interceptors allow us to create an application layer where data is unsafe, and vet and fix our incoming data there. That means that the rest of our application - including the API service - lives in a safe zone, where it's okay to make assumptions about the data it receives. The diagram below shows this idea for a typical container vs presentational component design.
The guiding principle is not one of place, but of direction.
An Interceptor is a beautiful place for this boundary when communicating with an API. It's a thin layer around the edge of our application, which none of our services or components needs to call directly. The adapters can become as complex as we need them to be, without it ever obfuscating our intent in the services and components that consume the data. Extracting adapters to a separate layer also enables us to unit test them separately.
Of course there are drawbacks as well. Relying on many interceptors - especially stacked interceptors that operate on the same requests - can make it more difficult to see what's going on. Interceptors should have a limited responsibility, and should never introduce unexpected behaviour.
In some cases adding a layer of interceptors is overkill. Indirection is it's own form of complexity, and interceptors need to be checked and maintained as well. They also add a decent amount of boilerplate 'just' - as in our case - to add an RxJS operator to a request.
Interceptors implement a simple interface with a single method called intercept. Interceptors are chained together in the order in which they are registered, allowing every HTTP request passing through to be changed along the way. We dove into the Angular source code to find out that the implementation follows the Chain of Responsibility pattern. Interceptors are wrapped in HttpHandlers by the HttpClientModule. The chain between HttpClient and HttpBackend is a chain of handlers, with HttpClient calling the first handler, and each handler calling the next.
We've seen how to create an Interceptor to convert the format of an API response. The recipe proved straightforward:
Finally, we discussed the effect this simple recipe can have on our application architecture. Interceptors allow us to add a conversion layer insulating the rest of our application from malformed data. Pushing complexity out to interceptors keeps the API service clean and declarative.
At the same time, added indirection and boilerplate does mean that an interceptor layer is overkill for small applications.
The verdict? With direction over place in mind, start out with just a service. But get comfortable writing interceptors. As the service and your application grows, you will want to iterate to push complexity out - and interceptors are the way to go.
Next up: We briefly mentioned unit testing, but we haven't taken a look at writing unit tests for interceptors yet. A situation that is remedied here!