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(() => {
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(() => {
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);
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é?
