The Challenge

Have you ever needed to apply distinct HTTP interceptors to different categories of requests in Angular? This exact problem came up in my work, and I discovered an elegant solution that leverages the inner workings of the HTTP layer.

This guide demonstrates how to implement custom versions of the standard HttpClient and attach module-scoped interceptors to each variant. This strategy promotes clean architecture through separation of concerns and simplifies handling of tricky edge cases.

To grasp the implementation, we first need to examine the internal mechanics of the HTTP layer. This knowledge is valuable and may spark ideas for solving other common challenges.

The Issue

HTTP interceptors debuted in Angular version 4.3. Since then, they've become a cornerstone for client-server communication in Angular applications.

These interceptors form a middleware layer between HttpClient and the browser's network interface, enabling modification or extension of every HTTP request and response passing through HttpClient. For a thorough explanation of this layer's architecture, check out this detailed analysis or consult the official Angular documentation for usage guidance.

Typical use cases for HttpInterceptor in Angular applications include:

  1. Adding headers to HTTP requests (such as authorization tokens or content type)
  2. Managing HTTP response errors at the application level
  3. Transforming HTTP responses before they reach the calling code
  4. Triggering UI effects like showing or hiding loading indicators during requests

Notice that the first item in that list was the source of my problem. My project required communication with multiple backend servers, and setting the Authorization header wasn't straightforward since each backend expected a different token format (Basic versus Bearer). This scenario might seem uncommon, but it arises frequently in applications integrating with various external data providers.

The cleanest solution would involve having two separate interceptor types, each handling its own token format and residing in the corresponding feature module:

@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 complication is that every request passes through both interceptors, making it ambiguous which token type gets applied since they both target the same Authorization header.

An alternative approach involves merging both interceptors into one and registering it at the root module level:

@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==`,
      },
    }));
 }
}

This method inspects the request URL and attaches the appropriate authorization token based on the path. However, it falls short in terms of performance, code quality, and encapsulation. The approach becomes unwieldy when dealing with more than two backends or complex header rules.

The Approach

I wanted to preserve the idea of having multiple interceptor types scoped to feature modules. Angular's dependency injection combined with class inheritance provided the breakthrough.

First, let's understand how HttpClient operates. It comes from @angular/common/http and is registered through HttpClientModule.

During instantiation, it receives a service implementing the HttpHandler interface via its 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 HttpHandler's primary duty is converting an HttpRequest into a stream of HttpEvents, with an HttpResponce typically among them. It's generally injectable, though not in our scenario — I'll clarify that shortly.

Since HttpClientModule doesn't expose a provider for handler inheritance, we need to create our own. Let's call it InterceptingHandler:

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 constructing the InterceptingHandler chain.

Don't mix up InterceptingHandler with InterceptorHandler — they serve different purposes. We're implementing the former, which routes requests to the first interceptor in line, allowing it to pass along to subsequent interceptors and finally reaching HttpBackend. The latter, InterceptorHandler, invokes the intercept method of each HttpInterceptor in sequence and returns the outcome.

Here's how that mechanism works:

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);
   }
}

With our InterceptingHandler ready, we can now address HttpClient. The strategy is to offer a specialized HttpClient service within a feature module, inheriting most functionality from the base HttpClient. The key distinction is that our version employs the custom InterceptingHandler to route requests through interceptors.

Implementation looks like this:

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()
    ));
  }
}

Observe how we leverage the HTTP_INTERCEPTORS token to collect interceptors associated with this particular HttpClient type. This is necessary because:

  1. It includes HttpXsrInterceptor, supplied by HttpClientModule, which shields us from cross-site request forgery attacks where unauthorized commands are submitted from a trusted user context.
  2. Other global tokens may exist that must apply to all requests regardless of origin
  3. Inheriting global interceptors is generally good practice

The FEATURE_HTTP_INTERCEPTORS token is designed for feature module scope. It aggregates module-level HttpInterceptor instances while keeping them separate from the global HTTP_INTERCEPTORS set.

Bringing It Together

As you're aware, services provided in eagerly-loaded modules are registered at the root level. When multiple providers share the same token, each subsequent declaration overrides the previous one — meaning the final declaration wins. Consequently, if we were to replace HttpClient using the provide option, we'd override the original for the whole application, which isn't desired. A separate token is therefore essential.

Here's how to employ a custom HttpClient type within 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() {};

This flow closely mirrors the standard pattern, with a few key distinctions:

  1. Feature interceptors are registered using the FEATURE_HTTP_INTERCEPTORS token.
  2. Declaring feature-level HttpInterceptor providers within the module is required.

Final Thoughts

Requests transmitted via FeatureHttpClient are handled exclusively by interceptors registered under the FEATURE_HTTP_INTERCEPTORS token. Meanwhile, interceptors configured with the standard HTTP_INTERCEPTORS token remain active for our custom FeatureHttpClient as well.

This pattern proves especially useful for implementing HTTP calls in third-party Angular Libraries, such as license validation. It eliminates concerns about application infrastructure and existing codebase.

Additionally, this technique works well for prepending API domains to requests when dealing with multiple servers. Feel free to explore this Stackblitz demonstration to see it in action.