Angular Circular dependency when inject TranslateService to interceptor

12.8k views Asked by At

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)
5

There are 5 answers

0
s1moner3d On

The solution proposed by @Aleš Doganoc is not working with Angualr 16, still getting circular DI error when requesting TranlsateService from Injector.

The best solution, as suggested by @Vahid, is changing HttpLoaderFactory dependency from HttpClient into HttpBackend and creating new HttpClient.

export function HttpLoaderFactory(httpHandler: HttpBackend): TranslateHttpLoader {
  return new TranslateHttpLoader(new HttpClient(httpHandler));
}

// ... 
imports: [
...,
TranslateModule.forRoot({
        loader: {
            provide: TranslateLoader,
            useFactory: HttpLoaderFactory,
            deps: [HttpBackend]
        },
        defaultLanguage: 'en'
    }),
...]
// ...
3
Aleš Doganoc On

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.

1
Aaron Ullal On

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)
    }
}
0
Alessandro Modica On

Ok , I think to resolve this problem of cyclic dependency! The problem is that the language gathered by translateservice, if source of browser or manual setting by a combobox changer, is a "behavior" value that need to be syncronize by all components (indipendently if a interceptor, resolver, other component, service, or another thing that in the future will exist...). And only solution to ensure that the lang value is recover well by all without problem of cyclic dependency, but on general, by nothing race condition problem is to implement a "LanguageService" that have a BehaviourSubject to track the lang value .

so the steps are:

a) implement the language service

    @Injectable({
  providedIn: 'root'
})
export class LanguageService {
  private currentLang = new BehaviorSubject<string>('it'); // Default language

  setCurrentLang(lang: string): void {
    this.currentLang.next(lang);
  }

  getCurrentLang(): Observable<string> {
    return this.currentLang.asObservable();
  }
}

The currentlang is the core of solution! get and set of this field its possibile to update the value by other subscriber (component that read the lang and produce data, and a resolver that consume data before to call, or to intercept a login, or others case use)

b) Use the service , on my case use is a component

constructor(private translateService: TranslateService, private languageService: LanguageService) {
  this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
    this.languageService.setCurrentLang(event.lang);
  });
}

On constructor of component or into a oninit phase, depend of your architecture and objective, set the lang by your translateservice as usual, and subscribe the value on languageservice implemented at first step.

c) finally consume the data on your application scope, that is a intercept of login, or to call backend with a resolver, that is my case use.

@Injectable({
  providedIn: 'root'
})
export class MyResolver implements Resolve<any> {
  constructor(private languageService: LanguageService) {}

  resolve(): Observable<any> | Promise<any> | any {
    return this.languageService.getCurrentLang().pipe(
      switchMap(lang => {
        // Utilizza `lang` per recuperare i dati internazionalizzati
      })
    );
  }

In this approach, there is not error cyclic dep, there is a deocoupled service that have a behavioursubject that have production and consumption by all subscriber, in this case to manage a lang .

I hope that is usefull to resolve your problem.

see u

1
Vahid On

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