1

I've created an HttpInterceptor that should globally handle errors via a generic retry strategy. Here's the implementation of the Interceptor:

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    return next.handle(request).pipe(
      retry({
        delay: genericRetryStrategy({
          maxRetryAttempts: 3,
          scalingDuration: 1000,
          includedStatusCodes: [500],
        }),
      }),
      catchError((error: HttpErrorResponse) => {
        return throwError(() => error);
      })
    );
  }
}

and here the custom operator function that handles retries:

export const genericRetryStrategy =
  ({
     maxRetryAttempts = 3,
     scalingDuration = 1000,
     includedStatusCodes = [],
   }: {
    maxRetryAttempts?: number;
    scalingDuration?: number;
    includedStatusCodes?: number[];
  } = {}) =>
    (attempts: Observable<any>) => {
      return attempts.pipe(
        filter((error) => !!includedStatusCodes.find((e) => e === error.status)),
        mergeMap((error, i) => {
          const retryAttempt = i + 1;
          // if maximum number of retries have been met
          // or response is a status code we don't wish to retry, throw error
          if (
            retryAttempt > maxRetryAttempts ||
            !includedStatusCodes.find((e: number) => e === error.status)
          ) {
            return throwError(error);
          }
          console.log(
            `Attempt ${retryAttempt}: retrying in ${
              retryAttempt * scalingDuration
            }ms`
          );
          // retry after 1s, 2s, etc...
          return timer(retryAttempt * scalingDuration);
        }),
        finalize(() => console.log('We are done!'))
      );
    };

I want to create some simple unit tests for my HttpInterceptor that:

  1. check whether it returns an error if the underlying retries fail with status code 500
  2. check that it returns a result if one of the retries was successful

Testing the operator function itself would be another topic but I've tried different approaches to achive the goals above but none of them have worked for me.

Traditional testing of Interceptor

The following test would actually cover goal one above but somehow I always get an error inside the operator retry function.

it('should retry 3 times and then throw an error if a 500 error is returned', fakeAsync(() => {
  const testUrl = '/api/test';

  httpClient.get(testUrl).subscribe(
    () => {
    },
    (error) => {
      console.log(error)
      expect(error.status).toBe(500);
    }
  );

  for (let i = 0; i < 4; i++) {
    const req = httpTestingController.expectOne(testUrl);
    expect(req.request.method).toBe('GET');
    req.flush(null, {status: 500, statusText: 'Server Error'});
    // Wait for the retry delay (i * scalingDuration)
    tick(i * 1000);
  }
}));

The error indicates that the value returned by next.handle() somehow is not an Observable. Throughout my research I've found that this problem occurs when using HttpTestingController but I couldn't find a fix that worked for me. Here's what the error looks like:

TypeError: attempts.pipe is not a function
        at /Users/tabmamab/Projects/PRA/por-user-portal-ui/libs/utils/src/lib/operator-functions.ts:92:23

Marble testing

Another approach I've tried was to mock the return value of next.handle() and assert the returned Observable stream through RxJS marbles but I've quickly discovered that the retry mechanism will not be called.

MauriceDev
  • 31
  • 7

0 Answers0