Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Exception Filters - zarządzanie kryzysami w aplikacji

W tym module poznasz zaawansowane techniki zarządzania wyjątkami w systemie opartym na NestJS - sprawa najwyższej wagi. Nasz legion musi być przygotowany na każdy kryzys, który może spotkać nas na niebezpiecznych rubieżach aplikacji. Dziś poznasz Exception Filters - zaawansowany system reagowania kryzysowego, który zapewni bezpieczeństwo całemu legionowi!

W rzymskim kastrum każdy kryzys wymaga szybkiej i precyzyjnej reakcji. Oblężenie, atak barbarzyńców, wyłom w murach - każda sytuacja ma swój protokół postępowania. Exception Filters w NestJS działają podobnie - automatycznie przechwytują błędy i reagują zgodnie z wcześniej ustalonymi procedurami.

Czym są Exception Filters?

Exception Filters to klasy implementujące interfejs

ExceptionFilter
, które:

  • Przechwytują wyjątki rzucane w aplikacji
  • Przetwarzają błędy w ujednolicony sposób
  • Formatują odpowiedzi błędów dla klientów
  • Logują incydenty dla celów diagnostycznych
  • Implementują strategię recovery gdy to możliwe
1import {
2 ExceptionFilter,
3 Catch,
4 ArgumentsHost,
5 HttpException,
6 HttpStatus,
7} from '@nestjs/common';
8import { Request, Response } from 'express';
9
10@Catch(HttpException)
11export class LegionaryHttpExceptionFilter implements ExceptionFilter {
12 catch(exception: HttpException, host: ArgumentsHost) {
13 const ctx = host.switchToHttp();
14 const response = ctx.getResponse<Response>();
15 const request = ctx.getRequest<Request>();
16 const status = exception.getStatus();
17 const exceptionResponse = exception.getResponse();
18
19 console.log(` Kryzys w systemie! Status: ${status}`);
20 console.log(` Miejsce: ${request.method} ${request.url}`);
21 console.log(` Szczegóły: ${exception.message}`);
22
23 const errorResponse = {
24 success: false,
25 statusCode: status,
26 timestamp: new Date().toISOString(),
27 path: request.url,
28 method: request.method,
29 message: this.getLegionaryErrorMessage(status),
30 details: typeof exceptionResponse === 'object' 
31 ? exceptionResponse 
32 : { message: exceptionResponse },
33 cohort: 'Equus Niger',
34 centurion: 'Caesar.js'
35 };
36
37 response.status(status).json(errorResponse);
38 }
39
40 private getLegionaryErrorMessage(status: number): string {
41 const messages = {
42 400: 'Błędny ładunek! Sprawdź swoje tributy przed wnoszeniem do systemu! ',
43 401: 'Brak autoryzacji. Wymagany ważny token JWT. ',
44 403: 'Brak uprawnień do tego zasobu. Potrzebujesz wyższej rangi! ',
45 404: 'Nie znaleziono tributu! Sprawdź mapę lub wybierz inną trasę! ',
46 409: 'Konflikt tributów! Inny legionariusz już pracuje z tym tributem! ',
47 429: 'Zbyt wiele żądań. Rate limit przekroczony. ',
48 500: 'Awaria w aplikacji! Mechanik już pracuje nad naprawą! '
49 };
50 
51 return messages[status] || 'Nieznany kryzys w systemie! Wszystkie ręce do systemu! ';
52 }
53}

Specialized Exception Filters

Database Exception Filter

Specjalny filtr dla błędów bazy danych:

1import { Catch, ExceptionFilter, ArgumentsHost } from '@nestjs/common';
2import { QueryFailedError, EntityNotFoundError, CannotCreateEntityIdMapError } from 'typeorm';
3
4@Catch(QueryFailedError, EntityNotFoundError, CannotCreateEntityIdMapError)
5export class DatabaseExceptionFilter implements ExceptionFilter {
6 catch(exception: any, host: ArgumentsHost) {
7 const ctx = host.switchToHttp();
8 const response = ctx.getResponse();
9 const request = ctx.getRequest();
10
11 let status = 500;
12 let message = 'Błąd w bazie tributów!';
13 let code = 'DATABASE_ERROR';
14
15 if (exception instanceof QueryFailedError) {
16 status = 400;
17 message = this.handleQueryError(exception);
18 code = 'QUERY_FAILED';
19 } else if (exception instanceof EntityNotFoundError) {
20 status = 404;
21 message = 'Nie znaleziono tributu w bazie!';
22 code = 'ENTITY_NOT_FOUND';
23 } else if (exception instanceof CannotCreateEntityIdMapError) {
24 status = 400;
25 message = 'Nieprawidłowy identyfikator tributu!';
26 code = 'INVALID_ENTITY_ID';
27 }
28
29 console.error(` Błąd bazy danych:`, {
30 error: exception.message,
31 query: exception.query,
32 parameters: exception.parameters,
33 url: request.url,
34 method: request.method
35 });
36
37 response.status(status).json({
38 success: false,
39 error: {
40 code,
41 message,
42 timestamp: new Date().toISOString(),
43 path: request.url
44 },
45 hint: this.getRecoveryHint(code)
46 });
47 }
48
49 private handleQueryError(exception: QueryFailedError): string {
50 const message = exception.message;
51 
52 if (message.includes('duplicate key')) {
53 return 'Ten tribut już istnieje w bazie! Każdy tribut musi mieć unikalny identyfikator.';
54 }
55
56 if (message.includes('foreign key constraint')) {
57 return 'Nie można usunąć tego elementu - jest połączony z innymi tributami!';
58 }
59
60 if (message.includes('not null constraint')) {
61 return 'Brakuje wymaganych informacji o tribucie!';
62 }
63
64 if (message.includes('check constraint')) {
65 return 'Dane nie spełniają reguł bazy tributów (np. wartość musi być dodatnia)!';
66 }
67
68 return 'Błąd w operacji na bazie tributów!';
69 }
70
71 private getRecoveryHint(code: string): string {
72 const hints = {
73 'QUERY_FAILED': 'Sprawdź czy wszystkie dane są poprawne i spróbuj ponownie',
74 'ENTITY_NOT_FOUND': 'Upewnij się że tribut istnieje lub stwórz nowy',
75 'INVALID_ENTITY_ID': 'Sprawdź format identyfikatora (powinien być liczbą)'
76 };
77 
78 return hints[code] || 'Skontaktuj się z centurionem o szczegóły problemu';
79 }
80}

Validation Exception Filter

Specjalistyczny filtr dla błędów walidacji:

1import { Catch, ExceptionFilter, ArgumentsHost, BadRequestException } from '@nestjs/common';
2import { ValidationError } from 'class-validator';
3
4@Catch(BadRequestException)
5export class ValidationExceptionFilter implements ExceptionFilter {
6 catch(exception: BadRequestException, host: ArgumentsHost) {
7 const ctx = host.switchToHttp();
8 const response = ctx.getResponse();
9 const request = ctx.getRequest();
10 const exceptionResponse = exception.getResponse();
11
12 // Sprawdź czy to błąd walidacji
13 if (this.isValidationError(exceptionResponse)) {
14 const validationErrors = this.extractValidationErrors(exceptionResponse);
15 
16 console.log(` Błędy walidacji dla ${request.url}:`, validationErrors);
17
18 response.status(400).json({
19 success: false,
20 error: 'VALIDATION_FAILED',
21 message: 'Twój ładunek nie przeszedł kontroli jakości!',
22 validationErrors,
23 timestamp: new Date().toISOString(),
24 path: request.url,
25 suggestions: this.generateSuggestions(validationErrors)
26 });
27 } else {
28 // Standardowy błąd 400
29 response.status(400).json({
30 success: false,
31 error: 'BAD_REQUEST',
32 message: 'Nieprawidłowe żądanie!',
33 details: exceptionResponse,
34 timestamp: new Date().toISOString(),
35 path: request.url
36 });
37 }
38 }
39
40 private isValidationError(response: any): boolean {
41 return response && 
42 Array.isArray(response.message) && 
43 response.message.length > 0 &&
44 typeof response.message[0] === 'object';
45 }
46
47 private extractValidationErrors(response: any): any[] {
48 if (!Array.isArray(response.message)) {
49 return [];
50 }
51
52 return response.message.map(error => ({
53 field: error.property,
54 value: error.value,
55 constraints: Object.values(error.constraints || {}),
56 children: error.children || []
57 }));
58 }
59
60 private generateSuggestions(validationErrors: any[]): string[] {
61 const suggestions = [];
62 
63 validationErrors.forEach(error => {
64 if (error.field === 'email') {
65 suggestions.push('Sprawdź format adresu email (example@legionariusze.com)');
66 }
67 
68 if (error.field === 'password') {
69 suggestions.push('Hasło musi mieć co najmniej 8 znaków i zawierać liczby');
70 }
71 
72 if (error.field === 'resourceValue') {
73 suggestions.push('Wartość tributy musi być liczbą większą od 0');
74 }
75 
76 if (error.field === 'coordinates') {
77 suggestions.push('Współrzędne muszą być w formacie: szerokość,długość');
78 }
79 });
80 
81 if (suggestions.length === 0) {
82 suggestions.push('Sprawdź wszystkie pola i upewnij się że dane są poprawne');
83 }
84 
85 return suggestions;
86 }
87}

All Exceptions Filter

Uniwersalny filtr przechwytujący wszystkie błędy:

1import {
2 ExceptionFilter,
3 Catch,
4 ArgumentsHost,
5 HttpException,
6 HttpStatus,
7 Logger
8} from '@nestjs/common';
9
10@Catch()
11export class AllExceptionsFilter implements ExceptionFilter {
12 private readonly logger = new Logger(AllExceptionsFilter.name);
13
14 catch(exception: unknown, host: ArgumentsHost): void {
15 const ctx = host.switchToHttp();
16 const response = ctx.getResponse();
17 const request = ctx.getRequest();
18
19 const httpStatus = exception instanceof HttpException
20 ? exception.getStatus()
21 : HttpStatus.INTERNAL_SERVER_ERROR;
22
23 const timestamp = new Date().toISOString();
24 const path = request.url;
25 const method = request.method;
26 const userAgent = request.headers['user-agent'] || 'Unknown';
27 const ip = request.ip;
28 const legionariusze = request.user || { name: 'Nieznany', rank: 'Gość' };
29
30 // Szczegółowe logowanie różnych typów błędów
31 if (exception instanceof HttpException) {
32 this.logger.warn(`HTTP Exception: ${exception.message}`, {
33 status: httpStatus,
34 path,
35 method,
36 legionariusze: legionariusze.name
37 });
38 } else if (exception instanceof Error) {
39 this.logger.error(`Unhandled Error: ${exception.message}`, {
40 stack: exception.stack,
41 path,
42 method,
43 legionariusze: legionariusze.name
44 });
45 } else {
46 this.logger.error('Unknown Exception', {
47 exception,
48 path,
49 method,
50 legionariusze: legionariusze.name
51 });
52 }
53
54 const errorResponse = {
55 success: false,
56 error: {
57 statusCode: httpStatus,
58 message: this.getErrorMessage(exception, httpStatus),
59 timestamp,
60 path,
61 method,
62 requestId: this.generateRequestId(),
63 },
64 context: {
65 cohort: 'Equus Niger',
66 centurion: 'Caesar.js',
67 legionariusze: legionariusze.name,
68 rank: legionariusze.rank,
69 userAgent,
70 ip: this.maskIpAddress(ip)
71 },
72 support: {
73 message: 'Jeśli problem się powtarza, skontaktuj się z centurionem',
74 reportBug: '/api/support/report-bug',
75 documentation: '/api/docs'
76 }
77 };
78
79 // W środowisku development dodaj stack trace
80 if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
81 errorResponse.debug = {
82 stack: exception.stack,
83 cause: exception.cause
84 };
85 }
86
87 response.status(httpStatus).json(errorResponse);
88 }
89
90 private getErrorMessage(exception: unknown, status: number): string {
91 if (exception instanceof HttpException) {
92 return exception.message;
93 }
94
95 const messages = {
96 400: 'Nieprawidłowe żądanie - sprawdź dane wejściowe',
97 401: 'Wymagana autoryzacja - zaloguj się ponownie',
98 403: 'Brak uprawnień - skontaktuj się z centurionem',
99 404: 'Zasób nie został znaleziony',
100 409: 'Konflikt - tributy jest obecnie używany',
101 422: 'Nie można przetworzyć żądania',
102 429: 'Zbyt wiele żądań - spróbuj później',
103 500: 'Wewnętrzny błąd serwera',
104 502: 'Błąd bramy - problem z zewnętrznym serwisem',
105 503: 'Serwis niedostępny - przerwa konserwacyjna',
106 504: 'Przekroczono czas oczekiwania'
107 };
108
109 return messages[status] || 'Wystąpił nieoczekiwany błąd';
110 }
111
112 private generateRequestId(): string {
113 return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
114 }
115
116 private maskIpAddress(ip: string): string {
117 if (!ip) return 'unknown';
118 
119 const parts = ip.split('.');
120 if (parts.length === 4) {
121 return `${parts[0]}.${parts[1]}.xxx.xxx`;
122 }
123 
124 return 'masked';
125 }
126}

Business Logic Exception Filter

Filtr dla specyficznych błędów biznesowych:

1export class LegionaryBusinessException extends Error {
2 constructor(
3 message: string,
4 public readonly code: string,
5 public readonly context?: any
6 ) {
7 super(message);
8 this.name = 'LegionaryBusinessException';
9 }
10}
11
12@Catch(LegionaryBusinessException)
13export class BusinessExceptionFilter implements ExceptionFilter {
14 catch(exception: LegionaryBusinessException, host: ArgumentsHost) {
15 const ctx = host.switchToHttp();
16 const response = ctx.getResponse();
17 const request = ctx.getRequest();
18
19 console.log(` Błąd biznesowy:`, {
20 code: exception.code,
21 message: exception.message,
22 context: exception.context,
23 url: request.url
24 });
25
26 const errorMap = {
27 'INSUFFICIENT_LEGION': {
28 status: 422,
29 message: 'Za mało legionistów do wykonania tej operacji!',
30 solution: 'Zrekrutuj więcej członków legionu'
31 },
32 'TRIBUTE_LOCKED': {
33 status: 423,
34 message: 'tribut jest zablokowany przez innego legionariusza!',
35 solution: 'Poczekaj aż operacja zostanie zakończona'
36 },
37 'COHORT_CAPACITY_EXCEEDED': {
38 status: 413,
39 message: 'Kohorta jest przeciążona! Nie można dodać więcej tributów!',
40 solution: 'Przenieś część tributów na inny fort'
41 },
42 'CURSED_TRIBUTE_WARNING': {
43 status: 406,
44 message: 'Próbujesz przenieść przeklęty tribut!',
45 solution: 'Potrzebujesz specjalnych uprawnień lub rytuału oczyszczenia'
46 },
47 'WEATHER_TOO_DANGEROUS': {
48 status: 503,
49 message: 'Pogoda jest zbyt niebezpieczna dla operacji!',
50 solution: 'Poczekaj na lepsze warunki atmosferyczne'
51 }
52 };
53
54 const errorInfo = errorMap[exception.code] || {
55 status: 400,
56 message: exception.message,
57 solution: 'Skontaktuj się z centurionem o szczegóły'
58 };
59
60 response.status(errorInfo.status).json({
61 success: false,
62 error: {
63 code: exception.code,
64 message: errorInfo.message,
65 context: exception.context,
66 solution: errorInfo.solution,
67 timestamp: new Date().toISOString(),
68 path: request.url
69 },
70 legionariusze: {
71 advice: this.getLegionaryAdvice(exception.code),
72 nextSteps: this.getNextSteps(exception.code)
73 }
74 });
75 }
76
77 private getLegionaryAdvice(code: string): string {
78 const advice = {
79 'INSUFFICIENT_LEGION': 'Pamiętaj: Jeden legionariusz to nie legion, trzech to początek kohorty!',
80 'TRIBUTE_LOCKED': 'Cierpliwość to cnota każdego doświadczonego legionariusza',
81 'COHORT_CAPACITY_EXCEEDED': 'Nawet największy fort ma swoje granice',
82 'CURSED_TRIBUTE_WARNING': 'Przeklęte tributy wymagają mądrości, nie tylko odwagi',
83 'WEATHER_TOO_DANGEROUS': 'Mądry centurion zna różnicę między odwagą a szaleństwem'
84 };
85 
86 return advice[code] || 'Każdy błąd to lekcja na przyszłość';
87 }
88
89 private getNextSteps(code: string): string[] {
90 const steps = {
91 'INSUFFICIENT_LEGION': [
92 'Odwiedź port rekrutacyjny',
93 'Sprawdź dostępność legionariuszów',
94 'Rozważ czasowe wynajęcie legionu'
95 ],
96 'TRIBUTE_LOCKED': [
97 'Sprawdź status operacji',
98 'Ustaw powiadomienie o zakończeniu',
99 'Sprawdź alternatywne tributy'
100 ],
101 'COHORT_CAPACITY_EXCEEDED': [
102 'Sprawdź dostępność innych kohort',
103 'Rozważ sprzedaż mniej wartościowych tributów',
104 'Ulepsz pojemność aktualnego fortu'
105 ]
106 };
107 
108 return steps[code] || ['Skontaktuj się z centurionem', 'Sprawdź dokumentację'];
109 }
110}

Rate Limiting Exception Filter

Specjalny filtr dla błędów rate limiting:

1import { ThrottlerException } from '@nestjs/throttler';
2
3@Catch(ThrottlerException)
4export class RateLimitExceptionFilter implements ExceptionFilter {
5 catch(exception: ThrottlerException, host: ArgumentsHost) {
6 const ctx = host.switchToHttp();
7 const response = ctx.getResponse();
8 const request = ctx.getRequest();
9
10 const retryAfter = this.calculateRetryAfter(request);
11 const legionariusze = request.user || { name: 'Nieznany' };
12
13 console.log(` Rate limit exceeded:`, {
14 legionariusze: legionariusze.name,
15 ip: request.ip,
16 url: request.url,
17 retryAfter
18 });
19
20 response
21 .status(429)
22 .header('Retry-After', retryAfter.toString())
23 .json({
24 success: false,
25 error: {
26 code: 'RATE_LIMIT_EXCEEDED',
27 message: 'Zbyt wiele żądań! Oddział nie może obsłużyć takiego natarcia!',
28 retryAfter,
29 timestamp: new Date().toISOString(),
30 path: request.url
31 },
32 legionariusze: {
33 name: legionariusze.name,
34 advice: 'Nawet najszybszy fort potrzebuje czasu na manewry',
35 suggestion: `Spróbuj ponownie za ${retryAfter} sekund`
36 },
37 limits: {
38 current: 'Przekroczony',
39 window: '1 minuta',
40 maxRequests: this.getMaxRequests(request.url)
41 }
42 });
43 }
44
45 private calculateRetryAfter(request: any): number {
46 // Różne limity dla różnych endpointów
47 const endpoint = request.url;
48 
49 if (endpoint.includes('/auth/')) {
50 return 60; // 1 minuta dla autoryzacji
51 }
52 
53 if (endpoint.includes('/tributes/')) {
54 return 30; // 30 sekund dla tributów
55 }
56 
57 return 15; // 15 sekund standardowo
58 }
59
60 private getMaxRequests(url: string): number {
61 if (url.includes('/auth/')) return 5;
62 if (url.includes('/tributes/')) return 10;
63 return 20;
64 }
65}

Rejestracja Exception Filters

1// main.ts - Globalne rejestracja
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4
5async function bootstrap() {
6 const app = await NestFactory.create(AppModule);
7 
8 // Globalne exception filters w kolejności od najbardziej specyficznych
9 app.useGlobalFilters(
10 new DatabaseExceptionFilter(),
11 new ValidationExceptionFilter(),
12 new BusinessExceptionFilter(),
13 new RateLimitExceptionFilter(),
14 new AllExceptionsFilter(),
15 );
16 
17 await app.listen(3000);
18}
19bootstrap();
20
21// Lub w module
22@Module({
23 providers: [
24 {
25 provide: APP_FILTER,
26 useClass: AllExceptionsFilter,
27 },
28 ],
29})
30export class AppModule {}
31
32// Na kontrolerze
33@Controller('tributes')
34@UseFilters(DatabaseExceptionFilter, ValidationExceptionFilter)
35export class TributeController {
36 // Na konkretnej metodzie
37 @Post('transfer')
38 @UseFilters(BusinessExceptionFilter)
39 transferTribute(@Body() transferDto: TransferTributeDto) {
40 // Implementation
41 }
42}

Integracja z systemami monitoring

1@Injectable()
2export class MonitoringExceptionFilter implements ExceptionFilter {
3 constructor(
4 private readonly loggingService: LoggingService,
5 private readonly metricsService: MetricsService,
6 private readonly alertingService: AlertingService
7 ) {}
8
9 catch(exception: any, host: ArgumentsHost) {
10 const ctx = host.switchToHttp();
11 const request = ctx.getRequest();
12 const response = ctx.getResponse();
13 
14 const status = exception instanceof HttpException 
15 ? exception.getStatus() 
16 : 500;
17
18 // Strukturalne logowanie
19 this.loggingService.logError({
20 timestamp: new Date().toISOString(),
21 level: 'ERROR',
22 message: exception.message,
23 stack: exception.stack,
24 context: {
25 method: request.method,
26 url: request.url,
27 userAgent: request.headers['user-agent'],
28 ip: request.ip,
29 user: request.user?.id
30 },
31 tags: ['exception', 'http', `status-${status}`]
32 });
33
34 // Metryki
35 this.metricsService.incrementCounter('http_errors_total', {
36 method: request.method,
37 status: status.toString(),
38 endpoint: request.route?.path || request.url
39 });
40
41 // Alerty dla krytycznych błędów
42 if (status >= 500) {
43 this.alertingService.sendAlert({
44 severity: 'HIGH',
45 title: `Server Error ${status}`,
46 description: exception.message,
47 context: {
48 url: request.url,
49 method: request.method,
50 timestamp: new Date().toISOString()
51 }
52 });
53 }
54
55 // Standardowa odpowiedź
56 response.status(status).json({
57 success: false,
58 error: exception.message,
59 timestamp: new Date().toISOString(),
60 path: request.url
61 });
62 }
63}

Exception Filters to Twoja ostatnia linia obrony przed chaosem w aplikacji. Dobrze skonfigurowane filtry potrafią przekształcić katastrofę w kontrolowaną sytuację, zapewniając legionowi i obywatelom bezpieczeństwo oraz jasne informacje o tym, co się dzieje.

Pamiętaj: dobry centurion jest przygotowany na każdy kryzys, a dobry Exception Filter na każdy błąd!

Ir a CodeWorlds