7

I have a problem with injecting dependencies into the interceptor. I want to inject TranslateService into HttpErrorInterceptor, but I get a cyclic dependency error. When I remove the TranslateService injection it all works.

I have declared interceptor in my app.module.ts. My app module look like this:

@NgModule({
 declarations: [
   AppComponent
 ],
 imports: [
   BrowserModule,
   BrowserAnimationsModule,
   CoreModule,
   HttpClientModule,
   TranslateModule.forRoot({
   loader: {
      provide: TranslateLoader,
      useFactory: HttpLoaderFactory,
      deps: [HttpClient],
   },
   defaultLanguage: 'pl-pl'
 }),
   AppRoutingModule,
   RouterModule,
   FormsModule,
   ReactiveFormsModule,
   ToastrModule.forRoot()
 ],
 providers: [
   {
     provide: HTTP_INTERCEPTORS,
     useClass: JwtInterceptor,
     multi: true
   },
   {
     provide: HTTP_INTERCEPTORS,
     useClass: HttpErrorInterceptor,
     multi: true,
     deps: [TranslateService, ToastrService]
   }
 ],
 bootstrap: [AppComponent]
})
export class AppModule { }

In AppModule I have imported CoreModule, where I have a folder with interceptors and my CoreModule looks like this:

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [
    CookieService,
    NoAuthGuard,
    AuthGuard
  ]
})
export class CoreModule { }

I put the login page in AuthModule, which looks like this:

@NgModule({
  declarations: [LoginComponent, AuthComponent, ForgotPasswordComponent],
  imports: [
    CommonModule,
    AuthRoutingModule,
    RouterModule,
    SharedModule
  ],
  providers: [
    AuthService
  ]
})
export class AuthModule { }

In Authmodule I have imported SharedModule, in which I have TranslateModule exported. And SharedModule look like this:

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    HttpClientModule,
    ReactiveFormsModule
  ],
  exports: [
    TranslateModule,
    ReactiveFormsModule
  ]
})
export class SharedModule { }

I can't find out why I have a cyclic dependency error on the login page.

My assumption is that I have imported CoreModule into AppModule, where I keep interceptors, guards and I have SharedModule, which improvises to all functional modules and I want to keep e.g. common components there.

Błąd, jaki dostaję to:

core.js:6162 ERROR Error: NG0200: Circular dependency in DI detected for InjectionToken HTTP_INTERCEPTORS. Find more at https://angular.io/errors/NG0200
    at throwCyclicDependencyError (core.js:216)
    at R3Injector.hydrate (core.js:11381)
    at R3Injector.get (core.js:11205)
    at HttpInterceptingHandler.handle (http.js:1978)
    at MergeMapSubscriber.project (http.js:1114)
    at MergeMapSubscriber._tryNext (mergeMap.js:44)
    at MergeMapSubscriber._next (mergeMap.js:34)
    at MergeMapSubscriber.next (Subscriber.js:49)
    at Observable._subscribe (subscribeToArray.js:3)
    at Observable._trySubscribe (Observable.js:42)
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
lukasbear
  • 121
  • 1
  • 5
  • It is likely that you have an indirect circular dependency. `A -> B -> C -> A` instead of a direct circular dependency `A -> B -> A` if that makes sense... – Get Off My Lawn Apr 18 '21 at 18:58
  • Perhaps unrelated, but is it on purpose that you have two identical provides? e.g. `provide: HTTP_INTERCEPTORS`. Also am a bit curious as to why you do need to rely on translation service within your interceptor, and finally maybe there are hints in the comments of this related SO https://stackoverflow.com/questions/48376571/cyclic-dependency-httpinterceptor-and-translateservice – cYrixmorten Apr 18 '21 at 19:06
  • @GetOffMyLawn It is possible, but I have no idea where it could occur. – lukasbear Apr 18 '21 at 19:27
  • @cYrixmorten As for the construction with HTTP_INTERCEPTORS, I have two different interceptors and from what I have seen, this is used with every definition of an interceptor. As for the translations in the interceptor, I want to use TranslateService to translate the errors that I get from the backend. – lukasbear Apr 18 '21 at 19:28
  • Just follow all of your imports to each file, eventually one will reference something that you already referenced higher up. – Get Off My Lawn Apr 18 '21 at 19:31

3 Answers3

12

The issue you have is that for the initialization of the TranslateModule you depend on the HttpClient which mean the HttpClientModule needs to be initialized first. This causes the initialization of your HttpErrorInterceptor because interceptors are initialized with the HttpClientModule initialization. This causes a cyclic dependency since your interceptor needs the TranslateService. You can workaround this by injecting the Injector in your HttpErrorInterceptor and then request the TranslateService on demand directly from the injector at the time you need it. This way you prevent the cyclic dependency on the initial initialization.

Since you did not provide code for your interceptor here is a sample interceptor that uses this approach.

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
  constructor(private readonly injector: Injector) {}

  public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    try {
      const translateService = this.injector.get(TranslateService)
      // log using translate service
    } catch {
      // log without translation translation service is not yet available
    }
  }
}

You still need to handle the case that getting the translate service fails since you can get an error on loading the translations.

bmurauer
  • 989
  • 8
  • 24
Aleš Doganoc
  • 11,568
  • 24
  • 40
10

According to this GitHub issue, some - myself included - were able to work around the problem by removing the defaultLanguage in TranslateModule.forRoot()

I have implemented my LanguageModule as follows:

@NgModule({
  imports: [
    HttpClientModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: (createTranslateLoader),
        deps: [HttpClient]
      },
      isolate: true
    })
  ],
  providers: [
    TranslateService
  ]
})
export class LanguageModule {
  public constructor(translateSvc: TranslateService, http: HttpClient) {
    translateSvc.onLangChange
      .pipe(
        switchMap((currentLang: LangChangeEvent) => zip(
          of(currentLang),
          http.get(`path/to/i18n/${currentLang.lang}.json`)
        ))
      ).subscribe(([currentLang, localizations]) => {
        translateSvc.setTranslation(translateSvc.currentLang, localizations, true);
      });

    translateSvc.use(translateSvc.getDefaultLang());
  }

And then imported it in my CoreModule:

@NgModule({
    imports: [
      CommonModule,
      HttpClientModule,
      BrowserAnimationsModule,
      LanguageModule,
      ...
    ],
    exports: [],
    declarations: [],
    providers: [
      ...
      {
        provide: HTTP_INTERCEPTORS,
        useClass: AuthInterceptor,
        multi: true
      }
    ]
  })
  export class CoreModule {
    public constructor(@Optional() @SkipSelf() parentModule: CoreModule, private translateSvc: TranslateService) {
      this.translateSvc.use(environment.defaultLang)
    }
}
Aaron Ullal
  • 4,855
  • 8
  • 35
  • 63
0

You can use HttpBackend instead of HttpClient. This way you'll not get "circular DI" error, and you'll bypass all the interceptors.

loader: {
  provide: TranslateLoader,
  useClass: TranslationLoader,
  deps: [HttpBackend],
},
type TranslateFile = Record<string, string>;

const httpRequest = new HttpRequest<TranslateFile>(
  'GET',
  `/assets/${lang}.json?v=${appVersion}`
);

return this._httpHandler.handle(httpRequest).pipe(
  filter((httpEvent) => httpEvent instanceof HttpResponse),
  map((httpResponse) => (httpResponse as HttpResponse<TranslateFile>).body!)
);
Vahid
  • 6,639
  • 5
  • 37
  • 61