Les applications modernes ne parlent plus au serveur de manière naïve. Une requête HTTP peut échouer pour mille raisons : timeout réseau, surcharge momentanée du serveur, perte de connectivité, rate limiting. Attendre que l'utilisateur rafraîchisse la page ou relance manuellement est inacceptable. Les intercepteurs HTTP d'Angular, combinés à une stratégie de retry intelligente, transforment une application fragile en système résilient qui absorbe les défaillances transitoires sans impacter l'UX. C'est la différence entre une app qui frustre et une qui inspire confiance.
Anatomie des intercepteurs HTTP
Les intercepteurs Angular s'insèrent dans la pipeline HTTP et interceptent chaque requête et réponse. Contrairement à un middleware backend, ils vivent côté client et contrôlent le flux entrant et sortant. Un intercepteur reçoit une HttpRequest , peut la modifier, la bloquer ou la passer au suivant via next.handle() . À la réponse, il peut inspecter le statut, capturer l'erreur ou transformer les données. Créer un intercepteur demande une classe implémentant HttpInterceptor et une injection via HTTP_INTERCEPTORS . La beauté du système réside dans l'ordre d'exécution : on peut empiler plusieurs intercepteurs et les chainer logiquement. Un intercepteur de logging, un autre de retry, un troisième d'auth—chacun joue son rôle sans connaître les autres. C'est une architecture micro-middleware côté client.
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log(`[HTTP] ${req.method} ${req.url}`);
return next.handle(req);
}
}Retry avec backoff exponentiel : la vraie résilience
Un simple retry linéaire (attendre 1s, 2s, 3s) ne suffit pas en production. Si le serveur est surchargé et qu'une centaine de clients retry simultanément avec le même délai, on crée une « thundering herd » qui tue le serveur. Le backoff exponentiel résout ce problème : attendre 100ms, puis 200ms, puis 400ms, puis 800ms... Les délais doublent à chaque tentative. Ajouter du jitter (une randomisation) empêche la synchronisation accidentelle entre clients. L'opérateur RxJS retryWhen permet de capturer les erreurs et de décider si on retry ou non. Combiner delay() avec un compteur et un plafond crée un système robuste et prévisible. En pratique, on retry les erreurs 5xx et les timeouts, mais pas les 4xx (sauf 429 pour le rate limiting).
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { retryWhen, mergeMap, finalize } from 'rxjs/operators';
@Injectable()
export class RetryBackoffInterceptor implements HttpInterceptor {
private maxRetries = 3;
private baseDelay = 100; // ms
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
retryWhen(errors =>
errors.pipe(
mergeMap((error, attempt) => {
// Ne retry que les erreurs 5xx et timeouts
if (!this.isRetryable(error) || attempt >= this.maxRetries) {
return throwError(() => error);
}
const delayMs = this.baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * delayMs * 0.1; // ±10%
return new Promise(resolve => setTimeout(resolve, delayMs + jitter));
})
)
)
);
}
private isRetryable(error: any): boolean {
return error.status >= 500 || error.status === 0; // 0 = timeout ou offline
}
}Circuit breaker : éviter la cascade de défaillances
Le retry backoff est puissant, mais insuffisant en cas de panne prolongée. Si le serveur est vraiment down, continuer à retry pendant 10 secondes ne fait que retarder l'inévitable et consommer des ressources. Un circuit breaker fonctionne comme un disjoncteur électrique : après N erreurs successives, il « coupe le circuit » et rejette immédiatement les requêtes pendant un délai (half-open state). Si le serveur revient, on bascule progressivement au mode normal. Implémenter un circuit breaker demande un peu plus de logique : tracker les erreurs, mémoriser l'état du circuit, et ajouter un timeout avant de tenter une reconnexion. La librairie ngx-circuit-breaker existe mais écrire le sien est instructif et customizable. Limiter les requêtes en vol quand le serveur est dégradé protège aussi le backend et améliore l'expérience utilisateur globale.
Pièges courants et anti-patterns
Le piège classique : retry *tous* les appels indifféremment. Un POST sans idempotence peut créer des doublons. Un 400 (bad request) ne s'améliore pas avec des retries. Un interceptor retry trop agressif retarde l'affichage des erreurs réelles à l'utilisateur. Autre piège : oublier le jitter. Si 1000 clients retry au même moment avec le même délai exponentiel, on synchronise les requêtes au lieu de les étaler. Le jitter (random ±10%) élimine ce problème. Enfin, ne pas monitorer les intercepteurs en production. Si un interceptor bugge silencieusement ou enregistre mal les tentatives, on n'a aucune visibilité sur ce qui se passe réellement. Ajouter des logs structurés (avec timestamp, endpoint, nombre de retries) aux interceptors est crucial. Utiliser un service d'observabilité (Datadog, New Relic) pour tracker les patterns de failure à l'échelle permet de détecter les problèmes systémiques avant qu'ils ne deviennent critiques.
Intégration avec l'authentification
Les interceptors jouent aussi un rôle clé en authentification. Un interceptor d'auth ajoute le token JWT à chaque requête. Si le token expire et que le serveur retourne 401, faut-il retry ? Oui, mais après avoir rafraîchi le token. Cela demande une coordination entre l'interceptor d'auth et celui de retry. Une approche : l'interceptor d'auth capture le 401, déclenche un refresh du token, et relance la requête originale. C'est plus efficace que de laisser l'interceptor de retry relancer bêtement. L'ordre d'enregistrement des intercepteurs importe : auth d'abord, puis retry, puis logging. Chaque interceptor doit être indépendant mais conscient des autres.
Tester les interceptors et les stratégies de retry
Tester un interceptor sans vraiment faire d'appels réseau est possible avec HttpClientTestingModule . On crée un mock du serveur avec HttpTestingController , on déclenche les requêtes, et on observe les retries. Pour valider le backoff exponentiel, on peut capturer les délais avec fakeAsync et tick . Vérifier que le jitter ajoute bien de la randomisation sans dépasser les limites. Tester aussi les cas limites : que se passe-t-il si toutes les tentatives échouent ? Si une tentative réussit au bout de 2 retries, la réponse est-elle correcte ? Les tests doivent couvrir les chemins happy path et failure path, et valider les délais.
Conclusion opérationnelle
Les intercepteurs HTTP avec retry backoff exponentiel transforment une application fragile en système résilient. L'implémentation n'est pas complexe : quelques dizaines de lignes de code RxJS. L'impact est énorme : absorber les défaillances transitoires sans code applicatif spécifique. Commencer simple (retry les 5xx avec backoff), monitorer en production, et évoluer vers un circuit breaker si nécessaire. La clé est de tester les failure paths, d'ajouter du jitter, et de ne pas retry aveuglément. Une bonne stratégie de retry est invisible à l'utilisateur mais sauve l'UX quand le réseau ou le serveur flanches.