HTTP Interceptors Angular : d’un outil global à une logique plus ciblée

Les HTTP Interceptors d’Angular permettent d’intercepter, modifier ou surveiller toutes les requêtes effectuées via le service HttpClient. Dans cet article, nous allons découvrir comment créer un intercepteur fonctionnel Angular, comprendre son fonctionnement et explorer plusieurs cas d’usage concrets : authentification, gestion d’erreurs, cache et monitoring.

L’intercepteur, un outil global

HttpClient est le service HTTP officiel d’Angular pour effectuer des requêtes HTTP vers des APIs. Ce service supporte un type de middleware connu sous le nom d’intercepteur. Il va donc pouvoir lire et modifier toutes les requêtes émises et traiter les réponses reçues par l’application Angular.

Schéma illustrant le fonctionnement d’un intercepteur

Le concept d’HTTP Interceptors a été intégré dans Angular dans la version 4.3. A ce moment, les intercepteurs reposaient uniquement sur l’écriture de classes implémentant l’interface HttpInterceptor couplée au système d’injection de dépendance d’Angular. Dans la version 15 d’Angular a été introduit le concept d’intercepteur fonctionnel. Bien que les deux types d’intercepteurs cohabitent, actuellement, ce sont les intercepteurs fonctionnels qui sont recommandés par Angular. S’inscrivant dans la stratégie globale d’Angular qui encourage l’écriture fonctionnelle (déclaration des routes, des guards, des resolvers, …), ils apportent un style plus léger améliorant la lisibilité.

Maintenant que l’on en sait plus sur le fonctionnement d’un intercepteur, nous allons créer un intercepteur qui va logguer nos requêtes. Nous utilisons la CLI qui va générer automatiquement un intercepteur fonctionnel (par défaut pour Angular 15+) :

npx ng generate interceptor logging

Le code suivant a alors été généré dans logging.interceptor.ts :

import { HttpInterceptorFn } from '@angular/common/http';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req);
};

Ici, notre fonction intercepte la requête émise par notre application Angular. La fonction next permet d’appeler le prochain intercepteur de la chaîne d’intercepteurs. Dans le cas où la chaîne se termine, la requête est alors émise.

On va modifier légèrement l’intercepteur pour logguer la requête émise :

import { HttpInterceptorFn } from '@angular/common/http';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log(req);
  return next(req);
};

next(req) renvoie un Observable de type Observable<HttpEvent<any>>. On peut alors utiliser cet observable pour logguer le code de retour de la réponse de notre requête HTTP.

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log(req);
  return next(req).pipe(
    tap((event) => {
      if (event instanceof HttpResponse) {
        console.log('Statut de la réponse:', event.status);
      }
    }),
  );
};

Pour que notre interceptor soit pris en compte par Angular, il est nécessaire de modifier la configuration :

bootstrapApplication(AppComponent, {providers: [
  provideHttpClient(
    withInterceptors([loggingInterceptor]),
  )
]});

Vous pouvez alors lancer votre application. Vous devriez voir dans vos logs les requêtes émises et les réponses associées en utilisant HttpClient.

Comme vous avez pu le voir dans la configuration, nous avons indiqué notre intercepteur dans un tableau. Il est possible d’indiquer plusieurs intercepteurs. L’ordre de la liste correspond à l’ordre dans lequel ils seront utilisés pour une requête sortante. Pour une requête entrante, ils seront exécutés dans le sens inverse. Si on indique l’ordre suivant : [interceptor1, interceptor2], on obtient le schéma suivant :

Schéma expliquant l’ordre d’exécution des intercepteurs

A noter que l’intercepteur HttpBackend est toujours présent pour gérer la communication avec le serveur.

Ainsi, en chaînant les intercepteurs, on va pouvoir appliquer différentes logiques sur toutes les requêtes.

Vers une logique plus ciblée

Les intercepteurs constituent un outil pouvant répondre à des problématiques variées. Nous avons déjà créé un intercepteur pour monitorer les requêtes, cependant il existe d’autres cas d’usages. Nous allons en détailler quelques-uns.

Cas d’usage 1 : Authentification

L’intercepteur peut être utilisé pour ajouter le Bearer Token lors de l’envoi de la requête afin d’avoir accès à un point de terminaison protégé de l’API.

import { inject } from '@angular/core';
import { AuthService } from './auth.service';
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();
  
  if (!token) {
    return next(req);
  }
  
  const authReq = req.clone({
    headers: req.headers.set('Authorization', `Bearer ${token}`)
  });
  
  return next(authReq);
};

On peut noter que pour modifier l’entête de la requête, on a cloné celle-ci. L’intercepteur est ainsi idempotent. En cas d’échec, on peut soumettre la même requête à la chaîne d’intercepteurs sans craindre des effets de bord. Il ne faut pas oublier que l’intercepteur s’applique sur toutes les requêtes. Ainsi, on pourrait l’améliorer pour appliquer l’ajout uniquement sur les points de terminaison privés.

import { inject } from '@angular/core';
import { AuthService } from './auth.service';
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const config = inject(APP_CONFIG);
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (isPublicUrl(req.url, config.publicApiPaths) || !token) {
    return next(req);
  }

  const authReq = req.clone({
    setHeaders: { Authorization: `Bearer ${token}` }
  });

  return next(authReq);
};

function isPublicUrl(url: string, publicPaths: string[]): boolean {
  return publicPaths.some((path) => url.includes(path));
}

Ici, la fonction matchesPublicUrl permet de nous assurer qu’un URL valide contient le segment /public/.

Cas d’usage 2 : Gestion des erreurs

On peut utiliser les intercepteurs pour la gestion des erreurs. Un cas d’usage est par exemple de vouloir rediriger l’utilisateur vers une page spéciale s’il accède à une API interdite pour lui.

import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';

export const redirectInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);
  
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 403) {
        router.navigate(['/forbidden']);
      }
      
      return throwError(() => error);
    })
  );
};

Afin d’éviter d’avoir à gérer des erreurs à chaque fois au moment de s’abonner à la réponse de la requête, il est possible de gérer les exceptions métier de notre API dans un intercepteur.

import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { NotificationService } from './notification.service';

export const customErrorInterceptor: HttpInterceptorFn = (req, next) => {
  const notificationService = inject(NotificationService);

  return next(req).pipe(
    catchError((errorResponse: HttpErrorResponse) => {
      
      if (errorResponse.error?.code) {
        switch (errorResponse.error.code) {
          case 'INVALID_CREDENTIALS':
            notificationService.error('Identifiants incorrects');
            break;
            
          default:
            notificationService.error(errorResponse.error.message || 'Erreur inconnue');
        }
      }
      
      return throwError(() => errorResponse);
    })
  );
};

L’intercepteur pourrait aussi être utilisé pour gérer le cas d’une erreur 401 quand le token JWT de connexion a expiré. On pourrait alors envoyer une requête pour récupérer le refresh token. Une fois ce nouveau token demandé, on renvoie la requête avec le token rafraichi.

Cas d’usage 3 : Loading et cache

Pour certaines requêtes où les données ne sont pas amenées à être changées régulièrement, il est possible de mettre en place un système de cache. Un cache va permettre de stocker les informations intéressantes d’une requête pour ne pas avoir besoin de les redemander au serveur. On économise alors du temps de calcul du serveur. Voici un exemple :

import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { CacheService } from '../cache.service';
import { inject } from '@angular/core';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';

export const cacheInterceptor : HttpInterceptorFn = (req, next) => {
  const cacheService = inject(CacheService);

  const cachedResponse = cacheService.get(req.urlWithParams);

  if (cachedResponse) {
    return of(cachedResponse);
  }

  return next(req).pipe(
    tap((event) => {
      if (event instanceof HttpResponse) {
        cacheService.set(req.urlWithParams, event);
      }
    }),
  );
};

Une amélioration possible serait d’affiner les requêtes à mettre en cache. Ici on cache tout type de requête : POST/PUT/DELETE.
Une manière d’effectuer cette amélioration est de fournir un contexte à la requête. Avec Angular, toutes les requêtes HTTP ont une propriété context qui contient une Map pour stocker des informations personnalisées.
On définit une instance de HttpContextToken comme clé du contexte.

export const CACHEABLE = new HttpContextToken<boolean>(() => false);

La fonction que l’on a transmise à notre clé permet de lui fournir une valeur par défaut à false.

Au moment de faire la requête que l’on souhaite mettre en cache, on va positionner la valeur associée à la clé CACHEABLE à true :

  return this.httpClient.get<EntityModel[]>(
      `${this.BASE_API_URL}/entity/${id}`,
      {
        context: new HttpContext().set(CACHEABLE, true),
      },
    );
  }

L’intercepteur devient alors :

import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { CacheService } from '../cache.service';
import { inject } from '@angular/core';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CACHEABLE } from '../../modules/endless-runner/services/game.service';

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  const cacheService = inject(CacheService);
  const shouldCache = req.context.get(CACHEABLE);

  if (!shouldCache) {
    return next(req);
  } else {
    const data = cacheService.get(req.urlWithParams);

    if (data) {
      return of(data);
    }

    return next(req).pipe(
      tap((event) => {
        if (event instanceof HttpResponse) {
          cacheService.set(req.url, event);
        }
      }),
    );
  }
};

À noter que, contrairement à d’autres propriétés de la classe HttpRequest, le context est mutable. C’est-à-dire que si l’on donne la même requête à la chaine d’intercepteur, l’intercepteur pourra observer que le contexte a muté. C’est notamment utile pour les retry.

Conclusion – De l’interception globale à la logique ciblée

Pour conclure, on peut distinguer deux types d’intercepteurs, dont un recommandé par Angular. Ce sont les intercepteurs fonctionnels pour leur clarté. Un intercepteur est un outil qui va pouvoir agir sur toutes les requêtes et les réponses HTTP de votre application. Il est alors possible de définir des intercepteurs avec des comportements précis pour traiter des problématiques différentes, par exemple :

  • Monitoring
  • Authentification
  • Gestion des erreurs
  • Gestion de cache

Vous souhaitez vous approprier les nouveautés des dernières versions d’Angular, comme par exemple les standalone components ou bien les signaux, ou encore migrer vers ces nouveaux concepts sur vos applications ?
Nos expert·e·s peuvent vous accompagner. Découvrez nos formations Angular 👉 https://www.4sh.fr/learning/formations/


NDLR :
Je tenais à remercier deux personnes qui ont également contribué à cet article :
– Merci à Yanick SERVANT pour sa relecture technique.
– Merci à Blandine HARDIVILLER pour sa relecture et son accompagnement dans le processus de publication.

En savoir plus sur 4SH Blog

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture