Search code examples
angularangular-http-interceptors

How to retry failed http requests for idempotent methods only?


I'm writing an Angular 10 app. I need to create an Interceptor that will retry all failed idempotent HTTP requests. After reading some online tutorials, I wrote an Interceptor that I thought would do the trick. Then I wrote a test to verify that it was working. Unforunately, my test will not fail; even when I modify the Interceptor to make the test fail, it doesn't fail.

Here's the Interceptor:

export class RetryInterceptor implements HttpInterceptor {

  // private IDEMPOTENT_METHODS = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'TRACE']; // this is the real prod code.
  private IDEMPOTENT_METHODS = ['POST']; // this should cause my test to fail, but it doesn't.

  constructor(private router: Router, private logger: NGXLogger) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('here'); // I've confirmed that this is being printed to the console.
    if (this.IDEMPOTENT_METHODS.includes(request.method)){
      console.log('there'); // I've confirmed that this is being printed to the console.
      return next.handle(request)
        .pipe(
          retry(1));
    }
    else{
      return next.handle(request);
    }
  }
}

Here's the test:

describe('RetryInterceptor', () => {

  let httpTestingController: HttpTestingController;
  let http: HttpClient;
  let router: Router;
  let location: Location;
  let fixture;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule,
        LoggerTestingModule,
        RouterTestingModule,
        TranslateModule.forRoot({
          loader: {
            provide: TranslateLoader,
            useFactory: httpTranslateLoader,
            deps: [HttpClient]
          }
        })
      ],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: RetryInterceptor,
          multi: true,
        },
      ],
    });

    httpTestingController = TestBed.inject(HttpTestingController);
    http = TestBed.inject(HttpClient);
    router = TestBed.inject(Router);
    location = TestBed.inject(Location);
    fixture = TestBed.createComponent(AppRootComponent);
    router.initialNavigation();
  });

  it('should not retry POSTs', fakeAsync( // I am expecting this to fail. 
    () => {
      http.post('/api/resource', {}).subscribe(response => expect(response).toBeTruthy());
      httpTestingController.expectOne({method: 'POST', url: '/api/resource'}).error(
        new ErrorEvent('network error', { message: 'bad request' }), { status: 400 });

      tick(); // the first tick is intended to make a call to the httpTestingController, which will return an error, which will be caught by my Interceptor.
      tick(); // on the second tick, my Interceptor should retry the failed call. this should, in turn, fail the test, because I have not set up a second expectation.
    })
  );
});

Can anyone spot what I'm doing wrong? Is my Interceptor broke? or is my test broke?


Solution

  • interceptor is fine. you need to verify your http testing controller at the end of your test to catch unexpected requests:

    // insert this at end of test or in an afterEach() block
    httpTestingController.verify()
    

    generally, any unit test relating to http requests should end with a verify.

    docs:

    https://angular.io/guide/http#testing-http-requests https://angular.io/api/common/http/testing/HttpTestingController#verify