{"community_link":"https://github.com/indepth-dev/content/discussions/30"}

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.

How to split HTTP Interceptors between multiple backends

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

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 :

@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:

@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

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:

// 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:

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:

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:

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

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:

@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

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.