2

I'm using Angular 10 and I have a few services and one interceptor that is causing:

ERROR Error: Uncaught (in promise): Error: Cannot instantiate cyclic dependency! InjectionToken HTTP_INTERCEPTORS Error: Cannot instantiate cyclic dependency! InjectionToken HTTP_INTERCEPTORS

app.module.ts

@NgModule({
  declarations: [
    AppComponent,
    DashboardComponent,
  ],
  imports: [
    AuthModule, // <= A custom module
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
  ],
  providers: [
    {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Interceptor

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  constructor(
    private responseHandler: HttpResponseHandlerService
  ) {
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        catchError((err: HttpErrorResponse, source) =>
          this.responseHandler.onCatch(err, source) // Catching all the errors here.
        )
      );
  }

}

HttpResponseHandlerService

@Injectable({
  providedIn: 'root',
})
export class HttpResponseHandlerService {

  constructor(
    // This is causing the error!
    private authService: AuthService
  ) {
  }

  public onCatch(response: HttpErrorResponse, source: Observable<any>): Observable<any> {
    return throwError(response);
  }

}

AuthModule

@NgModule({
  declarations: [
    LoginComponent
  ],
  imports: [
    SharedModule, // <= A shared module
    AuthRoutingModule
  ],
  providers: [
    AuthService
  ]
})
export class AuthModule {
}

AuthService

@Injectable()
export class AuthService extends HttpService {

  constructor(
    // Maybe this is causing the cyclic dependency ?
    http: HttpClient
  ) {
    super(http);
  }

  login(username: string, password: string) {
  }

}

SharedModule

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
  ],
  exports: [
    FormsModule,
    ReactiveFormsModule,
  ],
  providers: [
    HttpService // <= A custom service to handle HTTP requests
  ]
})
export class SharedModule {
}

HttpService

@Injectable()
export class HttpService {

  constructor(
    private http: HttpClient
  ) {
  }

  get<T>(options: any): Observable<T> {
  }

}

I have tried everything and I cannot get rid off the issue. I don't understand the origin of the error.

My goal is to fix the cyclic dependency issue correctly.

RRGT19
  • 1,437
  • 3
  • 28
  • 54

1 Answers1

4

I would guess that an HttpClient injects HTTP Interceptors. So this line:

    {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},

probably makes it so Angular will need to create an instance of ErrorInterceptor before it can create an HttpClient.

But ErrorInterceptor injects HttpResponseHandlerService, which injects AuthService, which injects HttpClient. So Angular's like:

new AuthService(
  new HttpClient([new ErrorInterceptor(
    new HttpResponseHandlerService(
      new AuthService(
         ... wait a minute.

The best way to fix this will depend somewhat on the nature of the dependencies between these different services.

Hopefully if you look at what HttpResponseHandlerService is planning to do with AuthService, it'll turn out you're calling a method that doesn't actually need the HttpClient. If that's the case, maybe you could refactor that method out to a separate service, which can be safely injected into HttpResponseHandlerService.

If that's not the case, you may need to consider other patterns like having AuthService get an HttpClient on-demand from a factory or service locator instead of injecting it directly.

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • Great explanation. The `AuthService` injects `HttpClient` because it extends `HttpService` and I'm forced to have that in my constructor to call `super(http)`. My `HttpService` is the service that has the original injection of `HttpClient`. How can I work on this? There is a way to avoid `AuthService` of having the `HttpClient` in the constructor and calling `super()` ? – RRGT19 Sep 22 '20 at 19:15
  • @RRGT19: That probably depends on what HttpResponseHandlerService is planning to do with AuthService (see my update). If you're going to respond to an error in one HTTP call by making another HTTP request, you may need to [use a Service Locator](https://stackoverflow.com/a/42462579/120955), but you should think really hard about the implications: what if _that_ request fails, so you respond by creating _another_ request, and so on and so forth, endlessly? – StriplingWarrior Sep 22 '20 at 19:29
  • Point understood. My goal with that service inside of `HttpResponseHandlerService` is to just call the `logout()` method when I receives 401 Unauthorized. What if use `this.injector.get(AuthService).logout();` ? this is a bad decision or it's a good alternative because in my case, I just want to call that single method on the entire `HttpResponseHandlerService`. – RRGT19 Sep 22 '20 at 19:32
  • 1
    Yeah, you could use that approach. You could also use a pub/sub model to emit an event when you detect that the user is logged out, and allow a separate subscriber to call logout when that happens. That would be a more loosely-coupled approach, which has its own advantages and disadvantages. – StriplingWarrior Sep 23 '20 at 16:00
  • Either way, be careful of your assumptions. Is it really true that you'll only get 401 Unauthorized when the user needs to be logged out? Couldn't this happen because the user simply tries to access something they no longer have permissions to? What happens if some kind of bug causes the logout action itself to return a 401? – StriplingWarrior Sep 23 '20 at 16:02