Nouvelle tentative de requêtes HTTP ayant échoué dans Angular

L'organisation de l'accès aux données du serveur est à la base de presque toutes les applications d'une page. Tout le contenu dynamique de ces applications est téléchargé depuis le backend.

Dans la plupart des cas, les requêtes HTTP au serveur fonctionnent de manière fiable et renvoient le résultat souhaité. Cependant, dans certaines situations, les demandes peuvent échouer.

Imaginez comment quelqu'un travaille avec votre site Web via un point d'accès dans un train qui parcourt le pays à une vitesse de 200 kilomètres par heure. La connexion réseau dans ce scénario peut être lente, mais les demandes du serveur, malgré cela, font leur travail.

Mais que faire si le train entre dans le tunnel? Il existe une forte probabilité que la connexion à Internet soit interrompue et que l'application Web ne puisse pas "atteindre" le serveur. Dans ce cas, l'utilisateur devra recharger la page d'application une fois le train sorti du tunnel et la connexion Internet rétablie.

Recharger la page peut affecter l'état actuel de l'application. Cela signifie que l'utilisateur peut, par exemple, perdre les données qu'il a entrées dans le formulaire.

Au lieu de simplement se réconcilier avec le fait qu'une certaine demande n'a pas abouti, il vaudrait mieux la répéter plusieurs fois et montrer à l'utilisateur une notification correspondante. Avec cette approche, lorsque l'utilisateur se rend compte que l'application essaie de faire face au problème, il ne rechargera probablement pas la page.



Le matériel, dont nous publions la traduction aujourd'hui, est consacré à l'analyse de plusieurs façons de répéter les demandes infructueuses dans les applications angulaires.

Répéter les demandes ayant échoué


Reproduisons une situation qu'un utilisateur travaillant sur Internet à partir d'un train peut rencontrer. Nous allons créer un backend qui traite la demande de manière incorrecte lors des trois premières tentatives pour y accéder, renvoyant uniquement les données de la quatrième tentative.
Habituellement, en utilisant Angular, nous créons un service, connectons le HttpClient et l'utilisons pour obtenir des données du 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;      })    );  } } 

Il n'y a rien de spécial ici. Nous HttpClient module Angular HttpClient et HttpClient une simple requête GET. Si la requête retourne une erreur, nous exécutons du code pour la traiter et renvoyons un Observable vide (objet observable) afin d'informer de ce qui a initié la requête. Ce code, pour ainsi dire, dit: "Il y avait une erreur, mais tout est en ordre, je peux le gérer."

La plupart des applications effectuent les requêtes HTTP de cette façon. Dans le code ci-dessus, la demande n'est exécutée qu'une seule fois. Après cela, il retourne les données reçues du serveur ou échoue.

Comment répéter la demande si le noeud final /greet pas disponible ou renvoie une erreur? Peut-être existe-t-il une instruction RxJS appropriée? Bien sûr que ça existe. RxJS a des opérateurs pour tout.

La première chose qui peut vous venir à l'esprit dans cette situation est l' retry . Regardons sa définition: «Renvoie un Observable qui joue l'Observable original sauf error . Si l'observable d'origine appelle l' error , cette méthode, au lieu de propager l'erreur, se réabonnera à l'observable d'origine.

Le nombre maximal de réabonnements est limité au count (il s'agit du paramètre numérique transmis à la méthode). "

La retry très similaire à ce dont nous avons besoin. Alors, intégrons-le dans notre chaîne.

 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()    );  } } 

Nous avons utilisé avec succès l'opérateur de nouvelle retry . Voyons comment cela a affecté le comportement de la requête HTTP qui est exécutée dans l'application expérimentale. Voici un grand fichier GIF qui montre l'écran de cette application et l'onglet Réseau des outils de développement du navigateur. Vous trouverez plusieurs autres démonstrations de ce type ici.

Notre application est extrêmement simple. Il fait simplement une requête HTTP lorsque le bouton PING THE SERVER est cliqué.

Comme déjà mentionné, le backend renvoie une erreur lors de l'exécution des trois premières tentatives pour lui exécuter une requête, et lorsqu'une quatrième requête lui parvient, il renvoie une réponse normale.

Dans l'onglet Outil du développeur réseau, vous pouvez voir que l' retry résout la tâche qui lui est affectée et répète l'exécution de la demande ayant échoué trois fois. La dernière tentative a réussi, l'application reçoit une réponse, un message correspondant apparaît sur la page.

Tout cela est très bien. Maintenant, l'application peut répéter les demandes ayant échoué.

Cependant, cet exemple peut encore être amélioré. Veuillez noter que les requêtes désormais répétées sont exécutées immédiatement après l'exécution des requêtes infructueuses. Ce comportement du système n'apportera pas beaucoup d'avantages dans notre situation - lorsque le train pénètre dans le tunnel et que la connexion Internet est perdue pendant un certain temps.

Nouvelle tentative retardée des demandes ayant échoué


Le train qui est entré dans le tunnel ne le quitte pas instantanément. Il y passe du temps. Par conséquent, nous devons «étirer» la période pendant laquelle nous effectuons des requêtes répétées au serveur. Vous pouvez le faire en différant les tentatives.

Pour ce faire, nous devons mieux contrôler le processus d'exécution des demandes répétées. Nous devons être en mesure de prendre des décisions sur le moment exact de répéter les demandes. Cela signifie que les capacités de l'opérateur de nouvelle retry ne nous suffisent plus. Par conséquent, nous nous tournons à nouveau vers la documentation sur RxJS.

La documentation contient une description de l' retryWhen , qui semble nous convenir. Dans la documentation, il est décrit comme suit: «Renvoie un observable qui lit l'observable d'origine à l'exception de l' error . Si l' error Observable d'origine appelle, alors cette méthode lancera Throwable, qui a provoqué l'erreur, l'Observable retourné par notifier . Si cet Observable appelle complete ou error , cette méthode appellera complete ou error sur l'abonnement enfant. Sinon, cette méthode se réabonnera à l'Observable d'origine. "

Oui, la définition n'est pas simple. Décrivons la même chose dans un langage plus accessible.

L' retryWhen accepte un rappel qui renvoie un Observable. L'observable renvoyé décide du retryWhen opérateur retryWhen fonction de certaines règles. À savoir, retryWhen comment retryWhen opérateur retryWhen :

  • Il cesse de fonctionner et gĂ©nère une erreur si l'Observable renvoyĂ© renvoie une erreur.
  • Il se ferme si l'observable retournĂ© signale l'achèvement.
  • Dans d'autres cas, lorsque l'observable revient avec succès, il rĂ©pète l'exĂ©cution de l'observable d'origine

Un rappel n'est appelé que lorsque l'Observable d'origine renvoie une erreur pour la première fois.

Nous pouvons maintenant utiliser ces connaissances pour créer un mécanisme de nouvelle tentative différée pour une demande ayant échoué à l'aide de l'instruction retryWhen .

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

Si l'Observable d'origine, qui est notre requête HTTP, renvoie une erreur, alors l'instruction retryWhen est retryWhen . Dans le rappel, nous avons accès à l'erreur qui a provoqué l'échec. Nous reportons les errors , réduisons le nombre de nouvelles tentatives et renvoyons un nouvel observable qui génère une erreur.

En fonction des règles de l' retryWhen , cet observable, car il retryWhen , retryWhen demande. Si la répétition échoue plusieurs fois et que la valeur de la variable retries diminue à 0, nous terminons la tâche avec une erreur qui s'est produite lors de l'exécution de la demande.

Super! Apparemment, nous pouvons prendre le code ci-dessus et remplacer l'opérateur de nouvelle retry dans notre chaîne par lui. Mais ici, nous ralentissons un peu.

Comment retries les retries variables? Cette variable contient l'état actuel du système de nouvelle tentative de demande ayant échoué. Où est-elle annoncée? Quand la condition est-elle réinitialisée? L'État doit être géré à l'intérieur du flux, pas à l'extérieur.

▍Créez votre propre instruction delayRetry


Nous pouvons résoudre le problème de la gestion des états et améliorer la lisibilité du code en écrivant le code ci-dessus en tant qu'opérateur RxJS distinct.

Il existe différentes façons de créer vos propres opérateurs RxJS. La méthode à utiliser dépend de la structure de l'opérateur concerné.

Notre opérateur est basé sur les opérateurs RxJS existants. En conséquence, nous pouvons utiliser le moyen le plus simple pour créer nos propres opérateurs. Dans notre cas, l'opérateur RxJs est juste une fonction avec la signature suivante:

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

Cette déclaration prend l'Observable d'origine et renvoie un autre Observable.

Étant donné que notre opérateur permet à l'utilisateur de spécifier la fréquence à laquelle les demandes répétées doivent être exécutées et le nombre de fois où elles doivent être exécutées, nous devons encapsuler la déclaration de fonction ci-dessus dans une fonction d'usine, qui prend delayMs (délai entre les maxRetry ) et maxRetry ( nombre maximum de répétitions).

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

Si vous souhaitez créer un opérateur qui n'est pas basé sur des opérateurs existants, vous devez faire attention à la gestion des erreurs et des abonnements. De plus, vous devrez étendre la classe Observable et implémenter la fonction lift .

Si vous êtes intéressé, jetez un œil ici .

Donc, sur la base des extraits de code ci-dessus, écrivons notre propre opérateur RxJs.

 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))        ))      )    ); } 

Super. Nous pouvons maintenant importer cet opérateur dans le code client. Nous l'utiliserons lors de l'exécution d'une requête HTTP.

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

Nous avons placé l'opérateur delayedRetry dans la chaîne et passé les paramètres 1000 et 3. Le premier paramètre définit le délai en millisecondes entre les tentatives de demandes répétées. Le deuxième paramètre détermine le nombre maximal de demandes répétées.

Redémarrez l'application et regardez comment fonctionne le nouvel opérateur.

Après avoir analysé le comportement du programme à l'aide des outils du développeur de navigateur, nous pouvons voir que l'exécution de tentatives répétées pour exécuter la demande est retardée d'une seconde. Après avoir reçu la bonne réponse à la demande, un message correspondant apparaîtra dans la fenêtre de l'application.

Répétition de demande exponentielle


Développons l'idée d'une nouvelle tentative différée des demandes ayant échoué. Auparavant, nous retardions toujours l'exécution de chacune des demandes répétées en même temps.

Ici, nous parlons de la façon d'augmenter le délai après chaque tentative. La première tentative de nouvelle tentative est effectuée après une seconde, la deuxième après deux secondes, la troisième après trois.

Créez une nouvelle instruction, retryWithBackoff , qui implémente ce comportement.

 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));          }        )))); } 

Si vous utilisez cet opérateur dans l'application et le testez, vous pouvez voir comment le délai d'exécution de la demande répétée augmente après chaque nouvelle tentative.

Après chaque tentative, nous attendons un certain temps, répétons la demande et augmentons le temps d'attente. Ici, comme d'habitude, une fois que le serveur a renvoyé la bonne réponse à la demande, nous affichons un message dans la fenêtre de l'application.

Résumé


La répétition des requêtes HTTP ayant échoué rend les applications plus stables. Ceci est particulièrement important lors de l'exécution de requêtes très importantes, sans les données obtenues grâce auxquelles l'application ne peut pas fonctionner normalement. Par exemple, il peut s'agir de données de configuration contenant les adresses des serveurs avec lesquels l'application doit interagir.

Dans la plupart des scénarios, l'instruction de nouvelle tentative RxJs retry pas suffisante pour fournir un système de nouvelle tentative fiable pour les demandes ayant échoué. L' retryWhen donne au développeur un niveau de contrôle plus élevé sur les demandes répétées. Il vous permet de configurer l'intervalle des demandes répétées. En raison des capacités de cet opérateur, il est possible de mettre en œuvre un schéma de répétition retardée ou des répétitions retardées exponentiellement.

Lors de l'implémentation de modèles de comportement pouvant être réutilisés dans les chaînes RxJS, il est recommandé de les formater en tant que nouveaux opérateurs. Voici le référentiel à partir duquel le code a été utilisé dans cet article.

Chers lecteurs! Comment résolvez-vous le problème de la nouvelle tentative de requêtes HTTP ayant échoué?

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


All Articles