Search code examples
angularrxjsinterceptorangular-httpclientretrywhen

How to retry when the API calls fails after subscribing in Angular 2+


I am calling an API to get random details. The problem is that, sometimes I am getting 502 error as bad gateway and it may also break due to a bad network connection also. Below is my code to API call

// COMPONENT API CALL SUBSCRIBE
     this._service.post('randomAPI/getdetails', filters).subscribe((response: any) => {
     this.itemList = response;    
   });

// SHARED SERVICE
     post<T>(url: string, body: any): Observable<T> {
     return this.httpClient.post<T>(url, body);
   } 

Whenever I get 500 or 502 server error, using an Interceptor I am routing to a error page to notify the user as server issue.

Instead, can I make the API to try one more time in component level or interceptor level if it fails and then route to error page?

// INTERCEPTOR
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(catchError(error => {
        if (error.status === 401 || error.status === 400 || error.status === 403) {
            this.router.navigateByUrl('abort-access', { replaceUrl: true });
        } else if (error.status === 500 || error.status === 502) {
                this.router.navigateByUrl('server-error', { replaceUrl: true });
        }
        return throwError("error occured");
     }));
    }

I saw few examples as they are using pipe and adding retryWhen() to achieve this. But as I am very new to angular, I am not able to figure out a way to do it.

Could anyone please help?


Solution

  • You can use the retryWhen operator. The principle behind this is that you throw an error when you don't want to retry.

    retryWhen is effectively a fancy catchError that will automatically retry unless an error is thrown.

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
      return next.handle(request).pipe(
        // retryWhen operator should come before catchError operator as it is more specific
        retryWhen(errors => errors.pipe(
          // inside the retryWhen, use a tap operator to throw an error 
          // if you don't want to retry
          tap(error => {
            if (error.status !== 500 && error.status !== 502) {
              throw error;
            }
          })
        )),
    
        // now catch all other errors
        catchError(error => {     
          if (error.status === 401 || error.status === 400 || error.status === 403) {
            this.router.navigateByUrl('abort-access', { replaceUrl: true });
          }
    
          return throwError("error occured");
        })
      );
    }
    

    DEMO: https://stackblitz.com/edit/angular-qyxpds

    Limiting retries

    The danger with this is that you will perform continuous requests until the server doesn't return a 500 or 502. In a real-world app you would want to limit retries, and probably put some kind of delay in there to avoid flooding your server with requests.

    To do this, you could use take(n) which will restrict your requests to n failed attempts. This won't work for you because take will stop the observable from proceeding to catchError, and you won't be able to perform navigation.

    Instead, you can set a retry limit and throw an error once the retry limit has been reached.

    const retryLimit = 3;
    let attempt = 0;
    
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
      return next.handle(request).pipe(
        // retryWhen operator should come before catchError operator as it is more specific
        retryWhen(errors => errors.pipe(
          tap(error => {
            if (++attempt >= retryLimit || (error.status !== 500 && error.status !== 502)) {
              throw error;
            }
          })  
        )),
    
        // now catch all other errors
        catchError(error => {     
          if (error.status === 401 || error.status === 400 || error.status === 403) {
            this.router.navigateByUrl('abort-access', { replaceUrl: true });
          } else if (error.status === 500 || error.status === 502) {
            this.router.navigateByUrl('server-error', { replaceUrl: true });
            // do not return the error
            return empty();
          }
    
          return throwError("error occured");
        })
      );
    }
    

    DEMO: https://stackblitz.com/edit/angular-ud1t7c