4

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?

Onera
  • 687
  • 3
  • 14
  • 34

2 Answers2

5

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

Kurt Hamilton
  • 12,490
  • 1
  • 24
  • 40
  • Can we use retryWhen inside a subscribe method when we call an API from a component? – Onera Feb 27 '20 at 10:30
  • No. Retry when happens in the pipe before your subscribe. You can add a pipe to any observable though. `observable.pipe(map()).pipe(map()).pipe(retryWhen()).subscribe()`. – Kurt Hamilton Feb 27 '20 at 10:32
  • So your component would do something like `this.service.makeRequest().pipe(retryWhen()).subscribe()` – Kurt Hamilton Feb 27 '20 at 10:33
  • I have one doubt- if I write this.service.makeRequest().pipe(retryWhen()).subscribe((resp) => { this.list = resp }); , you mean to say this.list will not be assigned with resp untill the retry is completed? – Onera Feb 27 '20 at 11:27
  • Yes. The `retryWhen` makes another request because you received an error, so there won't be a list to work with anyway. – Kurt Hamilton Feb 27 '20 at 11:29
  • I tried your code, it is not going to server-error page even it failed on second time. Am I missing something? – Onera Feb 27 '20 at 11:46
  • And also it is not entering to catchError block. – Onera Feb 27 '20 at 11:52
  • Ah yes. `take` will stop the observable. Let me have a look. – Kurt Hamilton Feb 27 '20 at 11:54
  • Updated. Instead of `take`, you can manually track the attempts in the `tap`. I'm sure there's a nicer solution, but this should get you started – Kurt Hamilton Feb 27 '20 at 12:19
1

Make this function

           delayedRetry(delayMs:number,maxRetry:number){

            let retries=maxRetry;

            return (src:Observable<any>)=>
            src.pipe(
           retryWhen((errors:Observable<any>)=>errors.pipe(
           delay(delayMs),
           mergeMap(error =>retries -- > 0 ? 
           of(error):throwError("Retrying..."))
     ))
   )
  
}

call this inside api url

    return this.http.get<RouteData[]>(url).pipe(
    this.delayedRetry(10000,4),
    catchError(err => {
   console.error(err);
    return EMPTY;
    }),
  shareReplay()
  );