How to split HTTP Interceptors between multiple backends

Post Editor

This article explains one of the possible ways to build different types of HttpClients for different feature modules (including non-lazy loaded) and associate a set of interceptors with each HttpClient type to ensure more reliable architecture.

5 min read
2 comments
post

How to split HTTP Interceptors between multiple backends

This article explains one of the possible ways to build different types of HttpClients for different feature modules (including non-lazy loaded) and associate a set of interceptors with each HttpClient type to ensure more reliable architecture.

post
post
5 min read
2 comments
2 comments

Have you ever wondered how to apply a different set of HTTP interceptors in Angular to different types of requests? That's the problem I faced at work and came up with an interesting solution that taps into the internal architecture of the HTTP layer.

This article will teach you how to implement different types of the standard HttpClient version and associate module scoped interceptors with each type. This approach enables clear architecture by following the principle of separation of concerns and makes it easy to tackle certain edge cases.

For you to understand the implementation we'll need to get into implementation details of the HTTP layer so bear with me. This knowledge is quite useful and might enable you to come up with other interesting solutions to common problems.

The problem
Link to this section

HTTP interceptors were introduced for the first time in Angular version 4.3. Since then, it is one of the most frequently used features in communication between client applications and HTTP servers.

It is a layer that sits in between HttpClient and the browser’s network API and allows modifying or extending every single HTTP request or response sent through HttpClient. You can find a great in-depth explanation of this layer architecture here or read the official documentation on the Angular website to understand how to use them.

Most Angular applications use HttpInterceptor  in the following ways:

  1. To set headers in HTTP requests (authorization tokens, content type, etc.)
  2. To handle HTTP response errors on the entire application level
  3. To modify HTTP response before it is returned to the calling code in the application
  4. And to even trigger visual side effects like showing and hiding spinners during request time

As you might have noticed, I highlighted the first item in the list and that's the problem I faced. In my project we needed to make requests against multiple backends and adding an 'Authorization' field to a request header wasn’t straightforward as each backend required different token types (Basic or Bearer). You might not have encountered such use case but it isn't rare in applications that have to deal with multiple external data sources.

The ideal approach to this problem would be to have two different types of interceptors each dealing with its own token type and belonging to corresponding feature modules :

<>Copy
@Injectable() export class BasicAuthTokenInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request.clone({ setHeaders: { Authorization: `Basic ZHN2ZA==`, }, })); } } @Injectable() export class BearerAuthTokenInterceptorimplements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request.clone({ setHeaders: { Authorization: `Bearer 3Hv2ZA==`, }, })); } } @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: BasicAuthTokenInterceptor, multi: true, }, ] }) class FirstFeatureSomeModule() {}; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: BearerAuthTokenInterceptor, multi: true, } ] }) class SecondFeatureModule() {};

The problem here is of course that each request will go through both interceptors and it's not clear which token type will be set as they both use the same header Authorization.

Another possible solution to the original problem is to combine two interceptors into one and provide the interceptor at the root level module:

<>Copy
@Injectable() export class AuthTokenInterceptorimplements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request.clone({ setHeaders: { Authorization: request.urlWithParams.startsWith(`https://first.feature/api`) ? `Basic ZHN2ZA==`, : `Bearer 3Hv2ZA==`, }, })); } }

Here we check the request path and add the required authorization token type depending on it. But it is not pretty optimal in terms of performance, clean code, and encapsulation. It's because we can have more than two backends or even have more sophisticated rules regarding request headers.

The solution
Link to this section

I wanted to stick to the approach with multiple types of interceptors for feature modules. And Angular's dependency injection mechanism with class inheritance is what helped me find the solution.

The first thing to understand is how HttpClient works. It is imported from @angular/common/http and provided in HttpClientModule.

When instantiating, it takes a service instance that implements the HttpHandler interface to the constructor:

<>Copy
// https://github.com/angular/angular/blob/master/packages/common/http/src/backend.ts export abstract class HttpHandler { abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>; } // https://github.com/angular/angular/blob/master/packages/common/http/src/client.ts export declare class HttpClient { constructor(handler: HttpHandler); // ... }

The main task of the HttpHandler is to transform HttpRequest into a stream of HttpEvents, one of which will likely be an HttpResponce. It’s injectable, but not in our case. I'll explain why it's so afterwards.

HttpClientModule doesn’t provide the class for handler inheritance, so we should implement it ourselves. Let's name it InterceptingHandler and it will look like this:

<>Copy
export class InterceptingHandler implements HttpHandler { private chain: HttpHandler; constructor(private backend: HttpBackend, private interceptors: HttpInterceptor[]) { this.buildChain() } handle(req: HttpRequest<any>): Observable<HttpEvent<any>> { return this.chain.handle(req); } private buildChain(): void { this.chain = this.interceptors.reduceRight((next, interceptor) => new InterceptorHandler(next, interceptor), this.backend ); } }

Here we're building the InterceptingHandler chain.

Do not confuse InterceptingHandler with InterceptorHandler. They have different responsibilities. We need to implement the former - InterceptingHandler. Its responsibility is to send requests to the first interceptor in the chain, which can pass it to the second interceptor, and so on, eventually reaching HttpBackend. InterceptorHandler in turn calls the intercept method of the HttpInterceptor in the chain and returns the result.

The code below demonstrates this behavior:

<>Copy
class InterceptorHandler implements HttpHandler { constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {} handle(req: HttpRequest<any>): Observable<HttpEvent<any>> { return this.interceptor.intercept(req, this.next); } }

Once we have our InterceptingHandler in place, we can tackle HttpClient. The idea is to provide a custom HttpClient service in a feature module that inherits most of the functionality from HttpClient. The difference with the standard HttpClient will be that we will use our custom InterceptingHandler to pass a request through interceptors.

Here's how we can do it:

<>Copy
const FEATURE_HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>( 'An abstraction on feature HttpInterceptor[]' ); @Injectable() class FeatureHttpClient extends HttpClient { constructor( backend: HttpBackend, @Inject(HTTP_INTERCEPTORS) interceptors: HttpInterceptor[], @Inject(FEATURE_HTTP_INTERCEPTORS) featureInterceptors: HttpInterceptor[], ) { super(new InterceptingHandler( backend, [interceptors, featureInterceptors].flat() )); } }

Note how we use HTTP_INTERCEPTORS token to retrieve a set of interceptors associated with this particular type of HttpClient. We need this because:

  1. It contains HttpXsrInterceptor which is provided by HttpClientModule and protects us from malicious exploits of a website where unauthorized commands are submitted from a user that the web application trusts.
  2. There are can be other global tokens, required for each request independently of the source
  3. It is a good approach to inherit global interceptors

FEATURE_HTTP_INTERCEPTORS is a token to use in a feature module scope. It combines module level HttpInterceptor and divides them from the global scope of HTTP_INTERCEPTORS.

Putting it all together
Link to this section

As you know, all services provided in non-lazy modules are registered at the root module and if multiple providers use the same token they are replaced by each next declaration in the module. In other words, the last declaration wins. This means that if we want to use a custom service as the HttpClient through provide option, we will override the original HttpClient for the entire application and we don't want that. That's why we need to use a separate token.

Here's how you would use a custom HttpClient type in a feature module:

<>Copy
@Injectable() class FeatureApiService { constructor( private readonly http: FeatureHttpClient ) {} getData(): Observable<any> { return this.http.get('...') } } @NgModule({ providers: [ BasicAuthTokenInterceptor, { provide: FEATURE_HTTP_INTERCEPTORS, useClass: BasicAuthTokenInterceptor, multi: true, }, FeatureHttpClient, FeatureApiService ] }) class FeatureModule() {};

It is pretty similar to the regular flow with several differences:

  1. Feature interceptors we should provide by FEATURE_HTTP_INTERCEPTORS token.
  2. Feature HttpInterceptor declaration in module is mandatory.

In conclusion
Link to this section

Requests, which are sent through FeatureHttpClient will be processed only by interceptors defined under the FEATURE_HTTP_INTERCEPTORS token. Interceptors that are defined using standard HTTP_INTERCEPTORS will be used by the custom FeatureHttpClient service as well.

This approach is a pretty good way to implement HTTP calls in third-party Angular Libraries, for example, to сheck for a license. It allows us to not think about application infrastructure and codebase.

As a benefit, we can use this for adding the API domain to each request in case with multiple servers. Check out this Stackblitz demo to explore it yourself.

Comments (2)

authorjbardon
21 April 2021

Hi, nice article it's a clever solution you found here! Never thought about it before.

Let me sum up to make sure I understood well: each feature module have its own HttpClient class and Interceptor token with a different name.

It means if I have two features FeatureA and FeatureB then I'll need: FeatureAHttpClient, FEATURE_A_HTTP_INTERCEPTORS and FeatureBHttpClient, FEATURE_B_HTTP_INTERCEPTORS. Even if they're all provided on AppModule (because they aren't lazy-loaded) this isn't an issue because the name is different.

A few questions about this solution.

  • I noticed the service is scoped to the feature module meaning we can't use it outside (in AppModule for instance). I think it also works with root scope if we manage to find a clever name for HttpClient and Interceptor token. Is it right?
  • The service needs to use a particular HttpClient, is there a way to enforce people use the right one?
  • Did you think about a solution for services used in several feature modules? Can the service have different Interceptors according to the module it's used in?

Also I felt in the article that implementing InterceptingHandler is mandatory but I don't find it in the StackBlitz, did I missed something?

authormgvWin
21 April 2021

Hi Jérémy, I am glad that my article was interesting for you.

You are right. Each module has its own HttpClient with Interceptor token.

Answer on your questions:

  • Yes. You can use with approach with clever names on the AppModule level
  • The one way is provide FeatureHttpClient on the module level, don't in the root. Another way use something like @Inject(HTTP_CLIENT) private http: HttpClient and provide FeatureHttpClient with HTTP_CLIENT token for the specific module level.
  • It depends on how you will use and import it. One important thing - this service can't be provided in root. If it is good for you, you can provide service in feature module (it creates a copy of service), provide some FeatureHttpClient by HTTP_CLIENT token in feature module declaration and use construction @Inject(HTTP_CLIENT) private http: HttpClient in your service, which is exported from top level.

Yes, you missed implementing. You can find it here

authorbriancodes
24 May 2021

I think the new HttpContext option available in v12 - basically a way of passing data metadata along with a Http requests - might be a solution to this problem. Good example by Netanel Basal in a recent article New in Angular v12 — Passing Context to HTTP Interceptors

authorwSedlacek
24 September 2021

Thank you for reminding me of this feature! I remember seeing Netanel Basal's post a few months back but had forgotten about it until you reminded me that it would solve this problem.

Share

About the author

author_image

Frontend Developer

author_image

About the author

Maksym Honchar

Frontend Developer

About the author

author_image

Frontend Developer

Featured articles