Skip to content

Retrying Failed HTTP Requests Using Exponential Backoff, RxJS, and HTTP Interceptors in Angular

September 29, 2023

Hugo Ayala

Network Timeouts (errors with status 0 and a ProgressEvent Object), Authentication Errors, and Internal Server Errors (error responses) are some of the most common situations whenever your front-end application calls a web service or external API. The end user needs to either see an error response to these types of errors, or they must implement a technique to retry later or with a certain delay for the call to be successful. Enter Exponential Backoff.

What is Exponential Backoff?

Exponential Backoff is a technique used to reduce the throttle rate of an endpoint call, giving it a delay before a retry following an exponential pattern which allows the endpoint sufficient time to reattempt failed calls.

When do you need to retry?

Normally you would want to retry any failed backend call. However, depending on how you have set up and controlled exceptions, transaction rollbacks, or API responses, you might want to skip retrying on a case-by-case basis.

Frontend or backend retry?

Conventionally, you would configure your backend to handle the retries and the Exponential Backoff behavior, since after all, this is the part that makes the logical backend call to the service, transforms the response, and sends it back to the front end and it works in most of the cases.

However, you must consider the advantages that functional programming gives you, such as that you can declaratively handle responses as a stream managed by an observable subscription, especially HTTP Service calls that are done and emitted only once. By combining a set of observables, from the frontend – you can achieve what could be a complicated set of classes and services in the backend – into a single, generic backoff utility that could be leveraged by all your service calls.

Getting Started

Set up a generic operator

You want the backoff operator to work with any service call, meaning that type safety needs to be set aside for the common good, which in this case is the Generic Type. That is why the original function – which was meant to handle AJAX responses – can be generic-typed to work with any service call within the Angular application.

export function backoff(retries: number = 2, delay: number = 250, excludedStatusCodes: number[] = defaultExcludedCodes): <T> (source: Observable<T>) => Observable<T> {
  return pipe(
    retryWhen((attempts) =>
      zip(range(1, retries + 1), attempts).pipe(
        mergeMap(([i, err]) => i > retries || excludedStatusCodes.length > 0 && excludedStatusCodes.find(statusCode => statusCode === err.status) ? throwError(err) : of(i)),
        map((i) => i * i),
        mergeMap((v) => timer(v * delay))
      )
    )
  );
} 

Combine and set up default parameters

You want to make the operator flexible enough so that it can run without any required parameters. However, if at a certain point a parameter is needed to define, you can add it to control:

  • Maximum tries
  • Delay in milliseconds
  • Error codes you might want to exclude from the retry

Add operator(s) to your services

RxJS operators are recommended to be tested using the marble testing approach. Once you’re sure that an operator behaves how you expect it to behave, you can begin to add them to your services. You can also decide if you want to set up the parameters or run the default configuration: 2 retries, 250 milliseconds delay, and no exclusion codes.

Use an interceptor to apply the operator

@Injectable({
  providedIn: 'root'
})

export class InterceptorService implements HttpInterceptor {

  constructor() { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if(req.method === 'GET' && !req.headers.has('skip-retry')) {
      if (req.headers.has('excluded-codes')){
        const excludedCodes: number[] = req.headers.get('excluded-codes').split(',').map(code => Number(code));
        return next.handle(req).pipe(backoff(2,250,excludedCodes));
      } else {
        return next.handle(req).pipe(backoff());
      }
    } else {
      return next.handle(req);
    }
  }
}

export const skipRetryHeader = new HttpHeaders().set('skip-retry','true');

Conclusion

RxJS provides a neat way to handle errors and gracefully retry failed attempts through the retry or retryWhen operator. Combining such operators with catchError or throwError helps you get a successful and carefully handled retry.

Resources

GOT A PROJECT?