Maîtrisez les intercepteurs fonctionnels d'Angular 15+ : syntaxe avec HttpInterceptorFn, withInterceptors(), gestion des erreurs, retry et comparaison avec l'ancien style classe.
Résumé : l'intercepteur fonctionnel
Introduit dans Angular 15, le style fonctionnel remplace la classe HttpInterceptor par une simple fonction typée HttpInterceptorFn. Cette approche est plus concise, tree-shakeable, et s'intègre naturellement dans les applications standalone.
Avantages par rapport à l'ancien style :
- Moins verbeux : une fonction au lieu d'une classe + décorateur + enregistrement complexe
- Tree-shakeable : l'intercepteur n'est inclus dans le bundle que s'il est utilisé
- inject() : injection de dépendances sans constructeur
- Pas de circular dependency : injecter
HttpClientdans l'intercepteur fonctionne sans problème - Idéal pour standalone : s'enregistre directement dans
provideHttpClient()
Syntaxe de base HttpInterceptorFn
Un intercepteur fonctionnel est une fonction qui prend la requête et le gestionnaire suivant, et retourne un observable.
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token') ?? '';
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(authReq);
};
HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => Observable<HttpEvent<unknown>>. Le typage est plus simple et direct qu'avec la classe.
Enregistrement avec withInterceptors()
Les intercepteurs fonctionnels s'enregistrent via withInterceptors() dans provideHttpClient(), directement dans main.ts ou app.config.ts.
// app.config.ts — application standalone
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor, // 1er
loggingInterceptor // 2ème
])
)
]
};
Pour une application avec AppModule (non-standalone) :
// app.module.ts avec intercepteur fonctionnel
import { NgModule } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
@NgModule({
providers: [
provideHttpClient(withInterceptors([authInterceptor]))
]
})
export class AppModule {}
withInterceptors([]) définit l'ordre d'exécution. Le premier intercepteur est le premier à traiter la requête sortante et le dernier à traiter la réponse entrante.
Injection de dépendances avec inject()
Sans constructeur, l'injection se fait directement dans le corps de la fonction avec inject(). Ceci fonctionne car les intercepteurs s'exécutent dans un contexte d'injection.
import { inject } from '@angular/core';
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const authService = inject(AuthService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
authService.logout();
router.navigate(['/login']);
}
if (error.status === 403) {
router.navigate(['/forbidden']);
}
return throwError(() => error);
})
);
};
HttpClient dans un intercepteur fonctionnel ne crée pas de dépendance circulaire. C'est l'un des grands avantages de cette approche.
Cas d'usage courants
1. Intercepteur de logging :
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { tap } from 'rxjs';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const start = Date.now();
console.info(`[HTTP] ${req.method} ${req.url}`);
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
console.info(`[HTTP] ${req.url} — ${Date.now() - start}ms`);
}
})
);
};
2. Intercepteur avec token refresh (HttpClient sans circular dependency) :
import { inject } from '@angular/core';
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 && !req.url.includes('/auth/refresh')) {
return authService.refreshToken().pipe(
switchMap(newToken => {
const retryReq = req.clone({
setHeaders: { Authorization: `Bearer ${newToken}` }
});
return next(retryReq);
})
);
}
return throwError(() => error);
})
);
};
3. Intercepteur conditionnel (skip sur certaines URLs) :
export const authInterceptor: HttpInterceptorFn = (req, next) => {
// Ne pas ajouter le token sur les requêtes publiques
if (req.url.includes('/public') || req.url.includes('/auth')) {
return next(req);
}
const token = localStorage.getItem('token') ?? '';
return next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
}));
};
Comparaison classe vs fonctionnel
Ancien style (classe) :
// auth.interceptor.ts — ancien style
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
return next.handle(req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
}));
}
}
// app.module.ts — enregistrement verbose
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
Nouveau style (fonctionnel) :
// auth.interceptor.ts — nouveau style
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
return next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
}));
};
// app.config.ts — enregistrement concis
provideHttpClient(withInterceptors([authInterceptor]))
Migrer d'un intercepteur classe
La migration est simple et peut se faire progressivement. Les deux styles coexistent : vous pouvez utiliser withInterceptors() et withInterceptorsFromDi() simultanément.
// Coexistence pendant la migration
provideHttpClient(
withInterceptors([authInterceptor]), // nouveaux
withInterceptorsFromDi() // anciens (HTTP_INTERCEPTORS)
)
Étapes de migration :
- Créer la nouvelle fonction
HttpInterceptorFnen parallèle - Remplacer
this.serviceparinject(Service) - Remplacer
next.handle(req)parnext(req) - Ajouter la fonction dans
withInterceptors([]) - Retirer l'ancienne classe de
HTTP_INTERCEPTORS - Supprimer la classe et son décorateur
@Injectable()
HttpInterceptorFn est disponible depuis Angular 15. Les deux styles fonctionnent jusqu'à Angular 20 inclus — la classe n'est pas dépréciée mais le style fonctionnel est officiellement recommandé.