Cours NestJS · Module 6: Gestion des erreurs et monitoring
Exception Filters - la gestion des crises
Dans cette leçon7
Un légionnaire demande une légion qui n'existe pas. Au lieu d'un refus lisible, il reçoit un mur de texte : le nom de la classe d'erreur, une pile d'appels, des chemins de fichiers de ton serveur. Un autre endpoint renvoie dans la même situation une forme de réponse totalement différente, parce que quelqu'un a traité l'erreur à la main. Le client ne sait pas à quoi s'attendre, et tu viens de montrer au monde le plan de ton camp.
Rome avait un bureau distinct pour les crises. Quand quelque chose échouait dans une province, le rapport n'arrivait pas au Sénat sous la forme reçue du terrain - un fonctionnaire le recopiait sur un formulaire uniforme. Dans NestJS, ce fonctionnaire est un Exception Filter.
Ce qu'il est, et ce qu'il n'est pas
Un Exception Filter est une classe qui intercepte les exceptions et met en forme la réponse d'erreur. C'est sa seule tâche, et il vaut la peine de la démarquer de ses voisins, car les quatre éléments se tiennent sur la route d'une requête :
- Middleware agit le plus tôt, sur la requête brute, avant que NestJS ne sache où elle va.
- Guard décide s'il faut laisser entrer - il renvoie
trueoufalse. - Pipe valide et transforme les données d'entrée.
- Exception Filter intervient en dernier, seulement quand quelque chose a échoué, et décide de ce que verra le client.
Les trois premiers travaillent sur la route vers la méthode du contrôleur. Le filtre travaille sur le chemin du retour, et uniquement si une exception est tombée en route.
@Catch - choisir ce dont on s'occupe
Un filtre commence par un décorateur indiquant quelles exceptions il traite :
1@Catch(HttpException)
2export class LegionHttpExceptionFilter implements ExceptionFilter {
3 catch(exception: HttpException, host: ArgumentsHost) {
4 const ctx = host.switchToHttp();
5 const response = ctx.getResponse<Response>();
6 const request = ctx.getRequest<Request>();
7
8 const status = exception.getStatus();
9
10 response.status(status).json({
11 statusCode: status,
12 timestamp: new Date().toISOString(),
13 path: request.url,
14 message: exception.message,
15 });
16 }
17}@Catch(HttpException) restreint le filtre à un seul type d'exception - celui-ci ne traitera que les exceptions HTTP et laissera passer tout le reste. Le décorateur nu, @Catch(), attraperait absolument tout ; c'est utile pour un filtre de dernier recours, mais tu perds alors l'information de type.
La méthode catch reçoit deux arguments. Le premier est l'exception elle-même. Le second, host de type ArgumentsHost, est le plus intéressant : NestJS sert non seulement HTTP mais aussi les WebSockets et les microservices, host conserve donc le contexte sous une forme indépendante du protocole. host.switchToHttp() en extrait le contexte HTTP, donnant accès aux objets Request et Response. Il ne bascule aucun protocole - il dit seulement « traite ce contexte comme du HTTP ».
Cinq étapes de traitement
Le code ci-dessus exécute toujours la même séquence, et elle mérite d'être connue en entier :
- Intercepter l'exception dans la méthode
catch(). - Récupérer le contexte HTTP par
switchToHttp(). - Lire le statut et les données de l'exception -
exception.getStatus(). - Mettre en forme la réponse JSON - une forme uniforme pour toute l'application.
- Envoyer la réponse au client par
response.status(...).json(...).
Note ce que cette réponse ne contient pas : ni pile d'appels, ni noms de fichiers. C'est délibéré - journalise la pile côté serveur, n'envoie au client que ce qui l'aidera à corriger sa requête. Les timestamp et path ajoutés ne coûtent rien et t'épargneront une heure de recherche quand un utilisateur signalera une erreur.
Les codes HTTP - la langue du refus
Le champ statusCode porte l'information la plus importante, choisis-le donc en conscience :
- 400 Bad Request - la requête est mal formée, le client doit la corriger.
- 401 Unauthorized - on ne sait pas qui tu es ; connecte-toi.
- 403 Forbidden - on sait qui tu es, et tu n'as pas le droit.
- 404 Not Found - la ressource n'a pas été trouvée.
- 500 Internal Server Error - c'est le serveur qui a failli, pas le client.
La frontière entre 4xx et 5xx compte ici : les quatre disent « corrige ta requête », les cinq disent « c'est de notre faute ». Renvoyer 500 pour des données d'entrée erronées envoie le client chercher une panne qui n'est pas chez lui.
La portée - où poster le fonctionnaire
Le même filtre peut se placer à trois niveaux, du plus étroit au plus large :
1// 1. Sur une méthode de contrôleur - le plus étroit
2@Get(':id')
3@UseFilters(LegionHttpExceptionFilter)
4findOne(@Param('id') id: string) { }
5
6// 2. Sur la classe du contrôleur - toutes ses méthodes
7@Controller('legions')
8@UseFilters(LegionHttpExceptionFilter)
9export class LegionsController { }
10
11// 3. Globalement - toute l'application
12app.useGlobalFilters(new LegionHttpExceptionFilter());La règle est simple : plus tu places le filtre bas, plus sa portée est étroite. Un filtre sur une méthode ne sert qu'elle, sur une classe - toutes les méthodes du contrôleur, un filtre global - l'application entière.
Retiens cet ordre, car il revient avec les guards, les pipes et les interceptors - dans NestJS, les quatre s'appliquent de la même façon. En pratique, un filtre global donne aux réponses leur forme commune, et des filtres plus étroits ajoutent un traitement propre à une portion de l'application.
Un filtre, plusieurs types d'exceptions
@Catch accepte plusieurs types à la fois - utile quand ils viennent d'une même source et demandent un traitement voisin :
1@Catch(QueryFailedError, EntityNotFoundError)
2export class DatabaseExceptionFilter implements ExceptionFilter {
3 catch(exception: Error, host: ArgumentsHost) {
4 const response = host.switchToHttp().getResponse<Response>();
5
6 const status =
7 exception instanceof EntityNotFoundError
8 ? HttpStatus.NOT_FOUND
9 : HttpStatus.BAD_REQUEST;
10
11 response.status(status).json({
12 statusCode: status,
13 message:
14 status === HttpStatus.NOT_FOUND
15 ? 'Ressource introuvable'
16 : 'Requête invalide vers la base de données',
17 });
18 }
19}Les deux exceptions viennent de TypeORM mais signifient des choses différentes, nous les distinguons donc dans catch par instanceof. EntityNotFoundError vaut 404 - le client a demandé quelque chose qui n'existe pas. QueryFailedError vaut 400, car elle découle en général des données envoyées par le client.
Et l'on voit ici la seconde valeur du filtre, au-delà de la forme uniforme : il traduit les exceptions internes dans la langue du HTTP. Le client n'a pas à savoir que tu utilises TypeORM - il reçoit un code et un message qui ont un sens pour lui. Tiens cette frontière : les noms de classes d'exceptions de ton code ne devraient jamais sortir de la maison.
Récapitulatif
Le bureau des crises fonctionne, les rapports partagent un formulaire :
- un Exception Filter est une classe qui intercepte les exceptions et met en forme la réponse d'erreur - ni Middleware, ni Guard, ni Pipe,
- ces trois-là travaillent sur la route vers le contrôleur, le filtre sur le retour et seulement après une exception,
@Catch(TypeException)restreint le filtre à ce type ;@Catch()sans argument attrape tout,host.switchToHttp()extrait le contexte HTTP d'ArgumentsHost, donnant accès àRequestetResponse- il ne bascule rien,- cinq étapes : intercepter, récupérer le contexte, lire le statut, mettre en forme le JSON, envoyer,
- n'envoie jamais la pile d'appels au client ;
timestampetpathdans la réponse te feront gagner du temps plus tard, - 404 signifie « ressource introuvable » ; 4xx dit « corrige ta requête », 5xx dit « c'est de notre faute »,
- portée du plus étroit :
@UseFilterssur une méthode,@UseFilterssur une classe,app.useGlobalFilters()globalement, @Catchaccepte plusieurs types ; à l'intérieur tu les distingues parinstanceofet les associes aux bons codes HTTP,- un filtre traduit les exceptions internes en HTTP - les noms de tes classes ne doivent pas sortir de la maison.
Dans la prochaine leçon, tu apprendras à créer tes propres exceptions, pour avoir quelque chose qui vaille la peine d'être intercepté - avec une hiérarchie lisible au lieu d'une poignée de throw new Error. En attendant, retiens ceci : un filtre est le fonctionnaire qui recopie un rapport sur un formulaire uniforme - le client reçoit un code et un message, et le plan du camp reste dans le camp.
Code de cette leçon : src/filters/exception-filters.ts
1// Exception Filters - Crisis Management in the Imperium
2// Interception et formatage des erreurs
3import {
4 ExceptionFilter,
5 Catch,
6 ArgumentsHost,
7 HttpException,
8 HttpStatus,
9 Logger,
10} from '@nestjs/common';
11import { Request, Response } from 'express';
12
13// ===========================================
14// 1. HttpException Filter de base
15// ===========================================
16
17@Catch(HttpException)
18export class LegionHttpExceptionFilter implements ExceptionFilter {
19 private logger = new Logger('LegionExceptionFilter');
20
21 catch(exception: HttpException, host: ArgumentsHost) {
22 const ctx = host.switchToHttp();
23 const response = ctx.getResponse<Response>();
24 const request = ctx.getRequest<Request>();
25 const status = exception.getStatus();
26 const exceptionResponse = exception.getResponse();
27
28 // Formatage de la réponse d'erreur - style Imperium
29 const errorResponse = {
30 statusCode: status,
31 timestamp: new Date().toISOString(),
32 path: request.url,
33 method: request.method,
34 message: typeof exceptionResponse === 'string'
35 ? exceptionResponse
36 : (exceptionResponse as any).message,
37 error: HttpStatus[status] || 'Unknown Error',
38 };
39
40 this.logger.error(
41 'Kryzys [' + status + '] ' + request.method + ' ' + request.url
42 + ': ' + errorResponse.message
43 );
44
45 response.status(status).json(errorResponse);
46 }
47}
48
49// ===========================================
50// 2. Filtre global - capture TOUTES les exceptions
51// ===========================================
52
53@Catch()
54export class AllExceptionsFilter implements ExceptionFilter {
55 private logger = new Logger('AllExceptions');
56
57 catch(exception: unknown, host: ArgumentsHost) {
58 const ctx = host.switchToHttp();
59 const response = ctx.getResponse<Response>();
60 const request = ctx.getRequest<Request>();
61
62 const status = exception instanceof HttpException
63 ? exception.getStatus()
64 : HttpStatus.INTERNAL_SERVER_ERROR;
65
66 const message = exception instanceof Error
67 ? exception.message
68 : 'Nieznany error w systemie Imperium';
69
70 this.logger.error('KRYTYCZNY BLAD: ' + message);
71
72 response.status(status).json({
73 statusCode: status,
74 timestamp: new Date().toISOString(),
75 path: request.url,
76 message: message,
77 });
78 }
79}
80
81// ===========================================
82// 3. Enregistrement des filtres
83// ===========================================
84
85// Globalement dans main.ts :
86// app.useGlobalFilters(new AllExceptionsFilter());
87//
88// Sur le contrôleur :
89// @UseFilters(new LegionHttpExceptionFilter())
90//
91// Sur la méthode :
92// @UseFilters(LegionHttpExceptionFilter)
93
94console.log('=== Exception Filters ===');
95console.log('@Catch(HttpException) - capture un type spécifique');
96console.log('@Catch() - capture TOUTES les exceptions');
97console.log('host.switchToHttp() - contexte HTTP');
98console.log('Enregistrement : global, contrôleur, méthode');
99Tu 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. Qu'est-ce qu'un Exception Filter dans NestJS ?
2. Que fait le décorateur @Catch(HttpException) sur une classe de filtre ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète la classe LegionHttpExceptionFilter avec @Catch(HttpException) qui implémente ExceptionFilter avec la méthode catch(exception, host), récupère response et request depuis ArgumentsHost et renvoie un JSON avec statusCode, timestamp, path et message
- Clique dans l'ordre
Ordonne correctement les étapes de gestion d'une exception dans un Exception Filter
- Tri vertical
Ordonne les niveaux d'application d'un Exception Filter de la portée la plus étroite à la plus large
- Éditeur de code
Complète le DatabaseExceptionFilter avec @Catch(QueryFailedError, EntityNotFoundError) qui différencie les types d'exception dans catch() et renvoie le code HTTP approprié (400 pour QueryFailed, 404 pour EntityNotFound)