📜 ⬆️ ⬇️

Repeat failed HTTP requests in Angular

Organizing access to server data is the foundation of almost any single-page application. All dynamic content in such applications is loaded from the backend.

In most cases, HTTP requests to the server work reliably and return the desired result. However, in some situations, requests may be unsuccessful.

Imagine someone working with your website through an access point on a train that travels around the country at a speed of 200 kilometers per hour. Network connection in this situation may be slow, but server requests, despite this, do their job.
')
And what if the train gets into the tunnel? There is a high probability that the connection with the Internet will be interrupted and the web application will not be able to "reach" the server. In this case, the user will have to reload the application page after the train leaves the tunnel and the Internet connection is restored.

Reloading the page can have an impact on the current state of the application. This means that the user may, for example, lose the data that he entered into the form.

Instead of simply accepting the fact that a certain request was unsuccessful, it would be better to repeat it several times and show the corresponding notification to the user. With this approach, when the user realizes that the application is trying to cope with the problem, he most likely will not reload the page.



The material, the translation of which we are publishing today, is devoted to the analysis of several ways to repeat unsuccessful requests in Angular-applications.

Repeat failed requests


Let's reproduce the situation that a user running on the Internet from a train may encounter. We will create a backend that processes the request incorrectly during the first three attempts to access it, returning data only on the fourth attempt.
Usually, using Angular, we create a service, connect the HttpClient and use it to get data from the backend.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      catchError(() => {        //           return EMPTY;      })    );  } } 

There is nothing special. We connect the Angular HttpClient module and perform a simple GET request. If the request returns an error, we execute some code to process it and return an empty Observable (observable object) in order to inform about it what initiated the request. This code as if says: "An error has occurred, but everything is in order, I will cope with it."

Most applications perform HTTP requests in this way. In the above code, the query is executed only once. After that, it either returns the data received from the server, or turns out to be unsuccessful.

How to repeat the request if the endpoint /greet not available or returns an error? Maybe there is a suitable RxJS operator? Of course he exists. RxJS has operators for just about anything.

The first thing that may come to mind in this situation is the retry operator. Let's look at its definition: “Returns an Observable that plays the original Observable with the exception of error . If the source Observable calls error , then this method, instead of propagating the error, will re-subscribe to the source Observable.

The maximum number of recurring subscriptions is limited to count (this is a numeric parameter passed to the method). "

The retry operator retry very similar to what we need. So let's embed it in our chain.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError, retry, shareReplay} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      retry(3),      catchError(() => {        //           return EMPTY;      }),      shareReplay()    );  } } 

We successfully used the retry operator. Let's look at how this affected the behavior of the HTTP request that is executed in the experimental application. Here is a large GIF file that demonstrates the screen of this application and the Network tab of the browser’s developer tools. You will meet here a few more such demonstrations.

Our application is very simple. It simply performs an HTTP request when you click on the PING THE SERVER button.

As already mentioned, the backend returns an error when the first three attempts to execute a request are made to it, and when the fourth request is received, it returns a normal response.

On the Network developer’s tools tab, you can see that the retry operator solves the task assigned to it and repeats the execution of the failed request three times. The last attempt is successful, the application receives a response, a corresponding message appears on the page.

Everything is very good. Now the application can repeat failed requests.

However, this example can still be improved. Please note that now repeated requests are executed immediately after the execution of requests that fail. This behavior of the system will not bring much benefit in our situation - when the train enters the tunnel and the Internet connection is lost for a while.

Delayed retry failed requests


The train, which hit the tunnel, does not leave it instantly. He spends some time there. Therefore, it is necessary to “stretch” the period during which we make repeated requests to the server. This can be done by postponing the execution of retries.

To do this, we need to better control the process of executing repeated requests. We need so that we can make decisions about when exactly we need to perform repeated requests. This means that the capabilities of the retry operator are not enough for us. Therefore, refer again to the RxJS documentation.

The documentation has a description of the operator retryWhen , which seems to suit us. The documentation describes it as follows: “Returns an Observable that reproduces the original Observable, with the exception of error . If the original Observable causes an error , then this method will throw a Throwable that caused the error, Observable, returned from the notifier . If this Observable calls complete or error , then this method will call complete or error on the child subscription. Otherwise, this method will re-subscribe to the original Observable. ”

Yes, the definition is not simple. Let's describe the same thing in a more accessible language.

The operator retryWhen accepts a callback, which is returned by the Observable. The returned Observable decides how the retryWhen operator retryWhen based on some rules. Namely, this is how the retryWhen operator retryWhen :


Callback is called only when the source Observable generates an error for the first time.

Now we can use this knowledge to create a pending retry mechanism for a failed request using the RxJS operator retryWhen .

 retryWhen((errors: Observable<any>) => errors.pipe(    delay(delayMs),    mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxEntry))    )) ) 

If the original Observable, which is our HTTP request, returns an error, then the retryWhen operator is retryWhen . In a callback, we have access to the error that caused the failure. We postpone errors , reduce the number of retries, and return a new Observable, which gives an error.

Based on the rules of the retryWhen operator, this Observable, since it produces a value, performs a repeat request. If the repetition is several times unsuccessful and the value of the variable retries reduced to 0, then we finish the work with an error that occurred during the execution of the request.

Wonderful! Apparently, we can take the code given above and replace with it the operator retry , which is in our chain. But here we will slow down a bit.

How to retries with variable retries ? This variable contains the current state of the system for retry failed requests. Where is she announced? When is the state reset? The state must be managed inside the stream, not outside it.

â–Ť Create your own delayedRetry operator


We can solve the problem of managing the state and improve the readability of the code by issuing the above code as a separate RxJS operator.

There are various ways to create your own RxJS operators. What method to use exactly depends on how this or that operator is arranged.

Our operator is based on existing RxJS operators. As a result, we can use the easiest way to create our own operators. In our case, the RxJs operator is just a function with the following signature:

 const customOperator = (src: Observable<A>) => Observable<B> 

This statement takes the original Observable and returns another Observable.

Since our operator allows the user to specify how often repeated requests should be performed, and how many times they need to be executed, we need to wrap the above function declaration into a factory function that takes the values ​​of delayMs (delay between repetitions) and maxRetry ( maximum number of repetitions).

 const customOperator = (delayMs: number, maxRetry: number) => {   return (src: Observable<A>) => Observable<B> } 

If you want to create an operator based on no existing operators, you need to pay attention to error handling and subscriptions. Moreover, you will need to extend the Observable class and implement the lift function.

If you are interested in it - look here .

So let's, based on the above code snippets, write your own RxJs operator.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up`; const DEFAULT_MAX_RETRIES = 5; export function delayedRetry(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        delay(delayMs),        mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxRetry))        ))      )    ); } 

Fine. Now we can import this operator into client code. We will use it when executing an HTTP request.

 return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(        delayedRetry(1000, 3),        catchError(error => {            console.log(error);            //               return EMPTY;        }),        shareReplay()    ); 

We put the delayedRetry operator in a chain and passed to it, as parameters, the numbers 1000 and 3. The first parameter specifies the delay in milliseconds between attempts to perform repeated requests. The second parameter determines the maximum number of repeated requests.

Restart the application and take a look at how the new operator works.

After analyzing the behavior of the program using the browser's developer tools, we can see that the execution of repeated attempts to fulfill the request is postponed for a second. After receiving the correct answer to the request, the corresponding message will appear in the application window.

Exponential delay request


Let's develop the idea of ​​deferred retry failed requests. Previously, we always postponed the execution of each of the repeated requests for the same time.

Here we will talk about how to increase the delay after each attempt. The first attempt to repeat the request is made in a second, the second - in two seconds, the third - in three.

Create a new operator, retryWithBackoff , that implements this behavior.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up.`; const DEFAULT_MAX_RETRIES = 5; const DEFAULT_BACKOFF = 1000; export function retryWithBackoff(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES, backoffMs = DEFAULT_BACKOFF) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        mergeMap(error => {            if (retries-- > 0) {              const backoffTime = delayMs + (maxRetry - retries) * backoffMs;              return of(error).pipe(delay(backoffTime));            }            return throwError(getErrorMessage(maxRetry));          }        )))); } 

If you use this operator in the application and test it - you can see how the delay in the execution of the repeated request grows after each new attempt.

After each attempt we wait for a certain time, repeat the request and increase the waiting time. Here, as usual, after the server returns the correct response to the request, we display a message in the application window.

Results


Repeating failed HTTP requests makes applications more stable. This is especially important when performing very important requests, without the data obtained through which, the application can not work normally. For example, it can be configuration data containing the addresses of the servers with which the application needs to interact.

In most scenarios of the operator RxJs retry not enough to organize a reliable system to repeat failed requests. The operator retryWhen gives the developer a higher level of control over repeated requests. It allows you to customize the interval for the execution of repeated requests. Thanks to the capabilities of this operator, you can implement a scheme of pending repetitions or repetitions with exponential shelves.

When implementing behavior patterns in RxJS chains that are suitable for reuse, it is recommended to design them in the form of new operators. Here is the repository, the code from which was used in this material.

Dear readers! How do you solve the task of repeating failed HTTP requests?

Source: https://habr.com/ru/post/459302/


All Articles