Aller au contenu

Réémettre une requête HTTP échouée à l’aide du retard exponentiel, de RxJS et des intercepteurs HTTP dans Angular

29 septembre 2023

Hugo Ayala

Les erreurs de temporisation (code d’état 0 et objet ProgressEvent), les échecs d’authentification et les erreurs internes du serveur sont particulièrement fréquents lorsque votre application frontale tente de rejoindre un service Web ou une API externe. Soit l’utilisateur doit voir un message d’erreur, soit il doit réessayer ultérieurement, ou au moins accorder un certain délai à l’application. C’est là que le retard exponentiel entre en jeu.

Qu’est-ce que le retard exponentiel?

Le retard exponentiel est une technique qui sert à réduire la limitation de bande passante d’un point de terminaison en lui octroyant un délai exponentiel entre les requêtes pour qu’il ait le temps de réémettre les appels échoués.

Quand faut-il réémettre une requête?

En temps normal, vous voudrez probablement réémettre toutes les requêtes qui ont échoué. Cela dit, selon la manière dont vous avez configuré et géré les exceptions, les annulations de transaction et les réponses des API, le faire chaque fois n’est peut-être pas une bonne idée.

Réémission frontale ou dorsale?

Par convention, on confie généralement la réémission de requête et le retard exponentiel à la partie dorsale de l’application, puisque c’est elle qui communique avec le service, transforme la réponse et la transfère en frontal. C’est une approche qui fonctionne la plupart du temps.

Pourtant, il faut considérer les avantages de la programmation fonctionnelle. Par exemple, elle permet de gérer les réponses de façon déclarative à la manière d’un flux traité par une inscription à un observable, notamment les appels à un service HTTP envoyés une seule fois. Vous pouvez accomplir la même chose qu’avec un assemblage complexe de classes et de services dorsaux en combinant simplement un ensemble d’observables depuis la partie frontale en un seul utilitaire générique de retard, qui est utilisable pour toutes vos requêtes de services.

Introduction pratique

Configurer un opérateur générique

Il s'agit de faire en sorte que l’opérateur de retard fonctionne pour toutes les requêtes de service. Il faut donc sacrifier la sûreté du typage, en l’occurrence, le type générique. C’est d’ailleurs la raison pour laquelle la fonction initiale, qui servait à gérer les réponses AJAX, peut être associée au type générique de manière à être compatible avec toutes les fonctions dans Angular.

export function backoff(retries: number = 2, delay: number = 250, excludedStatusCodes: number[] = defaultExcludedCodes): <T> (source: Observable<T>) => Observable<T> {
  return pipe(
    retryWhen((attempts) =>
      zip(range(1, retries + 1), attempts).pipe(
        mergeMap(([i, err]) => i > retries || excludedStatusCodes.length > 0 && excludedStatusCodes.find(statusCode => statusCode === err.status) ? throwError(err) : of(i)),
        map((i) => i * i),
        mergeMap((v) => timer(v * delay))
      )
    )
  );
} 

Combiner et configurer les paramètres par défaut

L’objectif est de rendre l’opérateur suffisamment flexible pour être exécuté sans paramètres requis. En revanche, s’il devient nécessaire de définir un paramètre, vous pouvez ajouter celui-ci aux exceptions. Par exemple :

  • Nombre maximum d’essais
  • Délai en millisecondes
  • Codes d’erreur à exclure du processus de réémission

Ajouter un ou plusieurs opérateurs à vos services

Il est recommandé de tester les opérateurs RxJS sur le principe des diagrammes de billes (marble testing). Une fois que vous êtes certains qu’un opérateur se comporte comme attendu, vous pouvez l’ajouter à vos services. Vous pouvez aussi choisir de configurer les paramètres vous-même ou d’utiliser les valeurs par défaut suivantes : 2 essais, délai de 250 millisecondes, aucun code d’erreur exclu.

Utiliser un intercepteur pour appliquer l’opérateur

@Injectable({
  providedIn: 'root'
})

export class InterceptorService implements HttpInterceptor {

  constructor() { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if(req.method === 'GET' && !req.headers.has('skip-retry')) {
      if (req.headers.has('excluded-codes')){
        const excludedCodes: number[] = req.headers.get('excluded-codes').split(',').map(code => Number(code));
        return next.handle(req).pipe(backoff(2,250,excludedCodes));
      } else {
        return next.handle(req).pipe(backoff());
      }
    } else {
      return next.handle(req);
    }
  }
}

export const skipRetryHeader = new HttpHeaders().set('skip-retry','true');

Conclusion

RxJS nous offre une méthode élégante pour gérer les erreurs et réémettre les requêtes échouées grâce aux opérateurs retry et retryWhen. Combiner ce type d’opérateur avec catchError ou throwError vous aidera à gérer rigoureusement le processus.

Ressources

VOUS AVEZ UN PROJET ?