Cours Angular · Module 9: HttpClient

Trois boucliers contre la panne - erreurs et cache

5 min de lecture
Dans cette leçon3

Le serveur ressemble parfois au temps qu'il fait sur un champ de bataille : une fois la connexion lâche, une fois la réponse arrive trop tard. Le samouraï ne tombe pas au premier faux pas - il porte des boucliers. Dans cette leçon tu en découvres trois : retry (retente encore une fois), catchError (transforme l'erreur brute en message compréhensible) et cache (ne demande pas deux fois la même chose). Ils viennent dans cet ordre, de la réaction à l'erreur jusqu'au travail que l'on évite.

Premier bouclier : retenter, puis traduire l'erreur

Beaucoup d'erreurs sont passagères - le réseau a cligné des yeux et une seconde plus tard tout remarche. Plutôt que d'abandonner tout de suite, retente la requête quelques fois, et ne transforme en message lisible que l'erreur qui s'obstine.

1getSamurai(id: number): Observable<Samurai> {
2  return this.http.get<Samurai>('/api/samurai/' + id).pipe(
3    retry({ count: 3, delay: 1000 }),   // 3 tentatives, une par seconde
4    catchError(this.handleError)
5  );
6}
7
8private handleError(error: HttpErrorResponse): Observable<never> {
9  let message = 'Erreur inconnue';
10  if (error.status === 0)        message = 'Pas de connexion au serveur';
11  else if (error.status === 404) message = 'Samurai introuvable';
12  else if (error.status >= 500)  message = 'Erreur serveur - recommence plus tard';
13  else if (error.error?.message) message = error.error.message;
14
15  return throwError(() => new Error(message));
16}

Tu suis ici deux boucliers à la fois. retry({ count: 3, delay: 1000 }) retente une requête échouée jusqu'à 3 fois avec un intervalle de 1000ms - si la panne était passagère, l'utilisateur ne remarque rien. Attention aux contresens : cet opérateur ne retarde pas la première requête, il n'annule rien au bout de 3 secondes et il n'envoie surtout pas 3 requêtes en parallèle - il rejoue la même requête, une par une, et seulement après un échec. Si malgré tout ça échoue, catchError appelle handleError. C'est bien catchError() l'opérateur RxJS qui gère les erreurs HTTP : handleError() n'est que le nom de ta méthode privée, tandis que onError() et errorHandler() n'existent tout simplement pas dans RxJS. Cette méthode reconnaît la cause d'après le statut, journalise l'erreur brute avec console.error, puis réémet un message humain à la place du HttpErrorResponse brut grâce à throwError(() => error). Regarde bien le statut 0 : ce n'est pas une réponse du serveur, c'est le statut que porte HttpErrorResponse quand le serveur n'a pas pu être atteint du tout - une erreur réseau. Ni 404, ni 500, ni 403 : ces trois-là signifient que le serveur a répondu, et méritent chacun leur branche, un 403 annonçant par exemple un accès refusé.

Deuxième bouclier : le cache, ou ne pas demander deux fois

Si dix composants réclament le même samouraï, pourquoi dix requêtes ? Le cache retient le flux déjà récupéré et le rend à tous ceux qui le demandent ensuite.

1export class CachedSamuraiService {
2  private cache = new Map<string, Observable<Samurai>>();
3
4  getSamurai(id: number): Observable<Samurai> {
5    const key = 'samurai-' + id;
6
7    if (!this.cache.has(key)) {
8      this.cache.set(key,
9        this.http.get<Samurai>('/api/samurai/' + id).pipe(
10          shareReplay({ bufferSize: 1, refCount: true })
11        )
12      );
13    }
14    return this.cache.get(key)!;
15  }
16}

Le cœur de l'affaire, c'est shareReplay({ bufferSize: 1 }) - c'est lui qui fait partir la requête une seule fois et rejoue ensuite son résultat à quiconque s'abonne plus tard. La Map cache garde ces flux tout prêts sous une clé, si bien qu'un deuxième getSamurai(5) ne touche plus le serveur : il rend le flux mémorisé. Sans shareReplay, chaque abonné déclencherait une nouvelle requête - c'est le piège le plus courant quand on met en cache à la main. L'option refCount: true libère le flux dès que le dernier abonné s'en va. Et parce qu'un cache doit aussi savoir oublier, donne-lui une méthode invalidate(id?) qui appelle cache.delete(key) pour une seule entrée ou cache.clear() pour tout vider - typiquement après un POST qui vient de modifier les données.

Troisième bouclier : un cache qui vieillit

Les données mémorisées finissent par se périmer. Tu ajoutes donc une durée d'expiration (TTL) : passé ce délai, le cache ira chercher des données fraîches tout seul.

1export class TimedCacheService {
2  private cache = new Map<string, { data$: Observable<any>; expiry: number }>();
3  private readonly TTL = 5 * 60 * 1000;  // 5 minutes
4
5  get<T>(url: string): Observable<T> {
6    const cached = this.cache.get(url);
7    if (cached && cached.expiry > Date.now()) {
8      return cached.data$;              // encore frais - renvoie depuis le cache
9    }
10
11    const data$ = this.http.get<T>(url).pipe(shareReplay({ bufferSize: 1 }));
12    this.cache.set(url, { data$, expiry: Date.now() + this.TTL });
13    return data$;
14  }
15}

Tout ce qui sépare ce cache du précédent tient dans une ligne de comparaison : cached.expiry > Date.now(). Si l'entrée a moins de cinq minutes, tu la rends telle quelle ; si elle est plus vieille, tu récupères de nouveau et tu réenregistres avec une date d'expiration fraîche. En TypeScript, on décrit volontiers cette paire par une petite interface CacheEntry<T> réunissant data$ et expiry - la Map se lit alors beaucoup mieux. Le compromis est raisonnable : tu épargnes le serveur sans jamais montrer des données vieilles d'une heure. Le choix du TTL dépend des données - une liste de clans peut vivre une heure, l'état d'une bataille quelques secondes à peine.

Retiens trois boucliers de cette leçon : retry te sauve d'une panne passagère, catchError traduit l'erreur en message, et le cache (avec shareReplay, éventuellement avec un TTL) épargne au serveur de refaire deux fois le même travail. Ensemble, ils rendent l'application aussi résistante qu'une armure.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel opérateur RxJS utilise-t-on pour gérer les erreurs HTTP ?

  2. 2. Quel statut a HttpErrorResponse quand le serveur n'a pas pu être atteint du tout (erreur réseau) ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Complète la gestion des erreurs dans le service avec journalisation et relance de l'erreur.

  • Tri vertical

    Ordonne les lignes consécutives d'un flux HTTP avec nouvelle tentative et interception d'erreur :

  • Clique dans l'ordre

    Construis l'appel qui réémet une erreur depuis le flux :

Articles utiles