Search code examples
angulardependency-injectionangular-httpclient

Separate HttpClient instances per module in Angular


In Angulars HttpInterceptor reference, I've found the following section:

To use the same instance of HttpInterceptors for the entire app, import the HttpClientModule only in your AppModule, and add the interceptors to the root application injector . If you import HttpClientModule multiple times across different modules (for example, in lazy loading modules), each import creates a new copy of the HttpClientModule, which overwrites the interceptors provided in the root module.

Fantastic, this is just what I need. So I can just import in each module the HttpClientModule so my ApiInterceptor which injects a base path and JWT token doesn't interfere with the HttpClient I use in the AppTranslationModule, which gets some translation files.

So I've my AppModule:

@NgModule({
  declarations: [AppComponent],
  imports: [AppRoutingModule, AppTranslationModule, CoreModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

... which imports AppTranslationModule and CoreModule. Each of them import HttpClientModule.

AppTranslationModule:

@Injectable()
export class TranslationLoader implements TranslocoLoader {
  constructor(private http: HttpClient) {}

  getTranslation(lang: string) {
    return this.http.get<Translation>(`/assets/i18n/${lang}.json`);
  }
}

@NgModule({
  imports: [TranslocoModule, HttpClientModule],
  providers: [
    {
      provide: TRANSLOCO_CONFIG,
      useValue: translocoConfig({
        availableLangs: ['en-US', 'de-CH', 'fr-CH', 'it-CH'],
        defaultLang: 'en-US',
        // Remove this option if your application doesn't support changing language in runtime.
        reRenderOnLangChange: true,
        prodMode: environment.production
      })
    },
    { provide: TRANSLOCO_LOADER, useClass: TranslationLoader }
  ],
  exports: []
})
export class AppTranslationModule {}

CoreModule:

@NgModule({
  imports: [BrowserModule, BrowserAnimationsModule, RouterModule, HttpClientModule],
  exports: [DefaultLayoutComponent],
  declarations: [DefaultLayoutComponent],
  providers: [
    { provide: BASE_API_URL, useValue: environment.api },
    { provide: HTTP_INTERCEPTORS, useClass: BaseUrlInterceptor, multi: true }
  ]
})
export class CoreModule {}

Unfortunately the interceptor (BaseUrlInterceptor) from the CoreModule is still applied to the HttpClient in the AppTranslationModule. If I understand the documentation mentioned above correctly, this shouldn't happen? Any ideas what is going on here?

I'm on Angular 11.


Solution

  • I've found a solution at this blog post. As mentioned there the HttpHandler needs to be changed, to create separate instances of HttpClient which are independent from the other interceptors:

    import { HttpBackend, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
    import { InjectionToken, Injector } from '@angular/core';
    import { Observable } from 'rxjs';
    
    export class CustomInterceptHandler implements HttpHandler {
      constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {}
    
      handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
        return this.interceptor.intercept(req, this.next);
      }
    }
    
    export class CustomInterceptorHandler implements HttpHandler {
      private chain: HttpHandler | null = null;
    
      constructor(private backend: HttpBackend, private injector: Injector, private interceptors: InjectionToken<HttpInterceptor[]>) {}
    
      handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
        if (this.chain === null) {
          const interceptors = this.injector.get(this.interceptors, []);
          this.chain = interceptors.reduceRight((next, interceptor) => new CustomInterceptHandler(next, interceptor), this.backend);
        }
    
        return this.chain.handle(req);
      }
    }
    

    With this the HttpClient can be extended:

    import { HttpBackend, HttpClient, HttpInterceptor } from '@angular/common/http';
    import { Injectable, InjectionToken, Injector } from '@angular/core';
    import { CoreModule } from '../core.module';
    import { CustomInterceptorHandler } from './custom-http.handler';
    
    export const API_HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>('API_HTTP_INTERCEPTORS');
    
    @Injectable({ providedIn: CoreModule })
    export class ApiHttpService extends HttpClient {
      constructor(backend: HttpBackend, injector: Injector) {
        super(new CustomInterceptorHandler(backend, injector, API_HTTP_INTERCEPTORS));
      }
    }
    

    Finally the new HttpClient along with the interceptors can be injected into the dependency tree:

    @NgModule({
      imports: [BrowserModule, BrowserAnimationsModule, RouterModule, HttpClientModule],
      exports: [DefaultLayoutComponent],
      declarations: [DefaultLayoutComponent],
      providers: [
        ApiHttpService,
        { provide: BASE_API_URL, useValue: environment.api },
        { provide: API_HTTP_INTERCEPTORS, useClass: BaseUrlInterceptor, multi: true },
        { provide: API_HTTP_INTERCEPTORS, useClass: ResponseTransformerInterceptor, multi: true }
      ]
    })
    export class CoreModule {}