Angular Circular dependency when inject TranslateService to interceptor
Asked Answered
R

6

10

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)
Ray answered 18/4, 2021 at 18:51 Comment(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...Bunkhouse
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 #48377071Thaine
@GetOffMyLawn It is possible, but I have no idea where it could occur.Ray
@Thaine 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.Ray
Just follow all of your imports to each file, eventually one will reference something that you already referenced higher up.Bunkhouse
W
19

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.

Wait answered 18/4, 2021 at 21:30 Comment(4)
Good explanation!Hairdo
great solution!Drown
not working with angular 16, still getting circular dependencies errorTradelast
@Tradelast for me, also on angular 16 it worked perfectly.Azobenzene
J
13

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)
    }
}
Jerid answered 29/9, 2021 at 9:47 Comment(1)
Simple and correct. defaultLanguage calls TranslateService behind scenes, this would be called in one place (only). Thank youDaughter
T
4

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'
    }),
...]
// ...
Tradelast answered 20/10, 2023 at 9:50 Comment(0)
U
1

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!)
);
Unideaed answered 15/8, 2023 at 2:18 Comment(1)
unluckly this approach dont resolve the problem! httpclient or httpbackend, if the translateservice is injected into another service or into a resolver, the error of cyclic dependency appear programmatically. I want to use the language setted on translateservice to send the information into a mongodb data that have just all documents i18n ready, but the info of lang I must to recover into an resolver, not into an component, and before the constructor of component. In other word I need to set the language at first call of resolver, and this cyclic dependency trouble my planAmparoampelopsis
A
0

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

Amparoampelopsis answered 24/2 at 13:22 Comment(0)
F
0

https://github.com/gilsdav/ngx-translate-router/issues/94#issuecomment-791584329

please check this one remove the defaultLanguage: 'en' config from the app.module.ts and move it to the app.component.ts

 constructor() {
    this.translateService.use('en');
 }
Faustena answered 29/4 at 9:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.