2

I'm developing a retry functionality automatically by a ErrorInterceptor implementing HttpInterceptor. I read that the retryWhen is exactly what I need. What I want it to happen is conditionally retry on the clicking of the Snackbar 'RETRY' button and I don't want it to fire unlimited requests (so, maybe after 3 times you can't try again). The problem is that now is that I don't know how to conditionally retry after the action button is clicked on the notification snackbar

I already tried to just fire the notificationService which displays the notification with the simple code:


intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      retryWhen(error => {
        this.notificationService.showNotification('problem RETRY', NotificationActionLabel.Retry, 4000, () => {
          console.log('retrying');
          return of(error);
        });
        return throwError(error);
      }),

This doesn't retry the functionality anymore, it just stops.

I then found some code about retryWhen which describes a generalRetryStrategy https://www.learnrxjs.io/operators/error_handling/retrywhen.html. I added this but I want it to fire conditionally (linked to the action button).

I have a callback function on the Notification Service

export class NotificationService {
  constructor(private snackBar: MatSnackBar) {}

  public showNotification(message: string, action?: string, duration?: number, callBackFunction?: Function) {
    const defaultDuration: number = 2500;
    const notificationRef = this.snackBar.open(message, action, {
      duration: duration == null ? defaultDuration : duration,
      panelClass: ['snackbar-styling'],
    });

    notificationRef.onAction().subscribe(result => callBackFunction(result));
  }
}

The interceptor is now as following:

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      retryWhen(this.genericRetryStrategy()),
      catchError((error: HttpErrorResponse) => {
        let errorMessage = '';

        // Client side error
        if (error.error instanceof ErrorEvent) {
          errorMessage = `Error: ${error.error.message}`;
        } else {
          // Server Side Error
          errorMessage = this.generateErrorMessage(error);
        }

        this.loggingService.logErrorMessage(error.error.message);
        this.notificationService.showNotification(errorMessage, null, 4000);
        return throwError(error.error.message);
      }),
    );

with the following function genericRetryStrategy, notice this is hardly any different than the code from learn-rxjs.

 genericRetryStrategy = ({
    maxRetryAttempts = 3,
    scalingDuration = 1000,
    excludedStatusCodes = [],
  }: {
    maxRetryAttempts?: number;
    scalingDuration?: number;
    excludedStatusCodes?: HttpStatusCode[];
  } = {}) => (attempts: Observable<any>) => {
    return attempts.pipe(
      mergeMap((error, i) => {
        this.notificationService.showNotification('attempting', 'retry', 4000, () => {
          const retryAttempt = i++;
          // if maximum number of retries have been met
          // or response is a status code we don't wish the retry, throw error
          if (retryAttempt > maxRetryAttempts || excludedStatusCodes.find(e => e === error.status)) {
            return throwError(error);
          }

          console.log(`Attempt ${retryAttempt}: retrying in ${retryAttempt * scalingDuration}ms`);

          //retry after 1s, 2s, etc...
          return timer(retryAttempt * scalingDuration);
        });
        return throwError(error);
      }),
      finalize(() => console.log('We are done!')),
    );
  };

I'm expecting it to only fire the retry functionality when the notification service callback function is really called (so the 'retry' button is clicked). Now it immediately skips the notification service call and just returns the error (throwError(error)).

enter image description here

Any help is much appreciated.

Danny Hoeve
  • 692
  • 13
  • 30
  • why are you not calling web service with the same data again. that will be much more simpler – TheParam Feb 12 '19 at 14:57
  • This is a generic way to handle error messages over the whole application, I don't want to call the function again. Also, the notification is called from the http interceptor not from the component. – Danny Hoeve Feb 12 '19 at 15:01
  • 1
    Did you ever figure this out? – theMayer Apr 12 '19 at 14:49
  • Nope, I thought about it some times but I don't think there is a good way of handling this. – Danny Hoeve Apr 12 '19 at 18:46
  • Have you tried returning the observable from snackBar rather than passing a callback function? i.e. in your notificationService return notificationRef.onAction() – Jon Catmull Aug 19 '19 at 09:04

1 Answers1

1

It skips it and returns error because as far as I can see that is what you are doing inside your mergeMap, you send a notification (with callback) and then return throwError(error);.

Untested code to follow.

Change notification service to return the observable:

export class NotificationService {
  constructor(private snackBar: MatSnackBar) {}

  public showNotification(message: string, action?: string, duration?: number, callBackFunction?: Function) {
    const defaultDuration: number = 2500;
    const notificationRef = this.snackBar.open(message, action, {
      duration: duration == null ? defaultDuration : duration,
      panelClass: ['snackbar-styling'],
    });

    notificationRef.onAction().subscribe(result => callBackFunction(result));
  }
}

then try something like this (perhaps drop the timer, currently it waits to show snackBar but personally this seems pointless as the snackbar Action should just trigger the retry IMO):

let genericRetryStrategy = ({
    maxRetryAttempts = 3,
    scalingDuration = 1000,
    excludedStatusCodes = [],
  }: {
    maxRetryAttempts?: number;
    scalingDuration?: number;
    excludedStatusCodes?: HttpStatusCode[];
  } = {}) => (attempts: Observable<any>) => {
    return attempts.pipe(
      mergeMap((error, i) => {
        const retryAttempt = i + 1;
        if (retryAttempt > maxRetryAttempts || excludedStatusCodes.find(e => e === error.status)) {
          return throwError(error);
        }
        console.log(`Attempt ${retryAttempt}: retrying in ${retryAttempt * scalingDuration}ms`);
        return timer(retryAttempt * scalingDuration).pipe(
          switchMap(this.notificationService.showNotification('attempting', 'retry', 4000))
        );
      }),
      finalize(() => console.log('We are done!')),
    );
  };
Jon Catmull
  • 11,873
  • 5
  • 19
  • 17