Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Exception Filters - zarządzanie kryzysami

Legionista pyta o legion, którego nie ma. Zamiast czytelnej odmowy dostaje ścianę tekstu: nazwę klasy błędu, ślad stosu, ścieżki plików z Twojego serwera. Kolejny endpoint zwraca w podobnej sytuacji zupełnie inny kształt odpowiedzi, bo ktoś obsłużył błąd ręcznie. Klient nie wie, czego się spodziewać, a Ty właśnie pokazałeś światu budowę swojego obozu.

Rzym miał na kryzysy osobny urząd. Gdy w prowincji coś zawiodło, meldunek nie szedł do Senatu w postaci, w jakiej przyszedł z pola - urzędnik przepisywał go na jednolity formularz. W NestJS tym urzędnikiem jest Exception Filter.

Czym jest, a czym nie jest

Exception Filter to klasa przechwytująca wyjątki i formatująca odpowiedź błędu. To jedyne jego zadanie i warto je odgraniczyć od sąsiadów, bo wszystkie cztery elementy stoją na drodze żądania:

  • Middleware działa najwcześniej, na surowym żądaniu, zanim NestJS wie, dokąd ono trafi.
  • Guard decyduje, czy wpuścić - zwraca
    true
    albo
    false
    .
  • Pipe waliduje i przekształca dane wejściowe.
  • Exception Filter wkracza jako ostatni, dopiero gdy coś poszło nie tak, i decyduje, co zobaczy klient.

Trzy pierwsze pracują na drodze do metody kontrolera. Filtr pracuje na drodze powrotnej, i to tylko wtedy, gdy w tej drodze padł wyjątek.

@Catch - wybór, czym się zajmujemy

Filtr zaczyna się od dekoratora mówiącego, jakie wyjątki obsługuje:

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)
zawęża filtr do jednego typu wyjątku - ten obsłuży tylko wyjątki HTTP, a wszystko inne przepuści dalej. Sam dekorator bez argumentu,
@Catch()
, złapałby absolutnie wszystko; to przydaje się dla filtra ostatniej szansy, ale wtedy tracisz informację o typie.

Metoda

catch
przyjmuje dwa argumenty. Pierwszy to sam wyjątek. Drugi,
host
typu
ArgumentsHost
, jest ciekawszy: NestJS obsługuje nie tylko HTTP, ale też WebSockety i mikroserwisy, więc
host
trzyma kontekst w postaci niezależnej od protokołu.
host.switchToHttp()
pobiera z niego kontekst HTTP
, dając dostęp do obiektów
Request
i
Response
. Nie przełącza żadnego protokołu - jedynie mówi "potraktuj ten kontekst jako HTTP".

Pięć kroków obsługi

Powyższy kod wykonuje zawsze tę samą sekwencję i warto ją znać jako całość:

  1. Przechwycenie wyjątku w metodzie
    catch()
    .
  2. Pobranie kontekstu HTTP przez
    switchToHttp()
    .
  3. Odczytanie statusu i danych wyjątku -
    exception.getStatus()
    .
  4. Sformatowanie odpowiedzi JSON - jednolity kształt dla całej aplikacji.
  5. Wysłanie odpowiedzi do klienta przez
    response.status(...).json(...)
    .

Zwróć uwagę, czego w tej odpowiedzi nie ma: śladu stosu ani nazw plików. To celowe - stos loguj po stronie serwera, klientowi wysyłaj tylko to, co pomoże mu poprawić żądanie. Dodane

timestamp
i
path
kosztują nic, a przy zgłoszeniu błędu od użytkownika oszczędzają godzinę szukania.

Kody HTTP - język odmowy

Pole

statusCode
niesie najważniejszą informację, więc dobierz je świadomie:

  • 400 Bad Request - żądanie jest źle zbudowane, klient musi je poprawić.
  • 401 Unauthorized - nie wiadomo, kim jesteś; zaloguj się.
  • 403 Forbidden - wiadomo, kim jesteś, i nie wolno Ci.
  • 404 Not Found - zasobu nie znaleziono.
  • 500 Internal Server Error - zawiódł serwer, nie klient.

Granica między 4xx a 5xx jest tu istotna: czwórki mówią „popraw żądanie", piątki mówią „to nasza wina". Zwracanie 500 przy błędnych danych wejściowych wysyła klienta na poszukiwanie usterki, której u niego nie ma.

Zasięg - gdzie postawić urzędnika

Ten sam filtr można umieścić na trzech poziomach, od najwęższego do najszerszego:

1// 1. Na metodzie kontrolera - najwęziej
2@Get(':id')
3@UseFilters(LegionHttpExceptionFilter)
4findOne(@Param('id') id: string) { }
5
6// 2. Na klasie kontrolera - wszystkie jego metody
7@Controller('legions')
8@UseFilters(LegionHttpExceptionFilter)
9export class LegionsController { }
10
11// 3. Globalnie - cała aplikacja
12app.useGlobalFilters(new LegionHttpExceptionFilter());

Reguła jest prosta: im niżej postawisz filtr, tym węższy jego zasięg. Filtr na metodzie obsłuży tylko ją, na klasie - wszystkie metody kontrolera, globalny - całą aplikację.

Zapamiętaj tę kolejność, bo wraca przy guardach, pipe'ach i interceptorach - w NestJS wszystkie cztery stosuje się tak samo. W praktyce jeden filtr globalny nadaje wspólny kształt odpowiedzi, a filtry węższe dokładają obsługę specyficzną dla wycinka aplikacji.

Jeden filtr, kilka typów wyjątków

@Catch
przyjmuje wiele typów naraz - przydaje się, gdy pochodzą z jednego źródła i wymagają podobnej obsługi:

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          ? 'Nie znaleziono zasobu'
16          : 'Nieprawidłowe zapytanie do bazy danych',
17    });
18  }
19}

Oba wyjątki pochodzą z TypeORM, ale znaczą co innego, więc wewnątrz

catch
rozróżniamy je przez
instanceof
.
EntityNotFoundError
to 404 - klient poprosił o coś, czego nie ma.
QueryFailedError
to 400, bo zwykle wynika z danych, które klient przysłał.

I tu widać drugą wartość filtra, poza jednolitym kształtem: tłumaczy wyjątki wewnętrzne na język HTTP. Klient nie musi wiedzieć, że używasz TypeORM - dostaje kod i komunikat, które coś dla niego znaczą. Trzymaj się tej granicy, @name: nazwy klas wyjątków z Twojego kodu nie powinny nigdy wyjść na zewnątrz.

Podsumowanie

Urząd kryzysowy działa, meldunki mają jeden formularz:

  • Exception Filter to klasa przechwytująca wyjątki i formatująca odpowiedź błędu - nie Middleware, nie Guard, nie Pipe,
  • trzy tamte pracują na drodze do kontrolera, filtr - na powrotnej i tylko po wyjątku,
  • @Catch(TypWyjatku)
    zawęża filtr do wskazanego typu;
    @Catch()
    bez argumentu łapie wszystko,
  • host.switchToHttp()
    pobiera kontekst HTTP z
    ArgumentsHost
    , dając dostęp do
    Request
    i
    Response
    - niczego nie przełącza,
  • pięć kroków: przechwyć, pobierz kontekst, odczytaj status, sformatuj JSON, wyślij,
  • do klienta nie wysyłaj śladu stosu;
    timestamp
    i
    path
    w odpowiedzi oszczędzą Ci później czasu,
  • 404 to „nie znaleziono zasobu"; 4xx znaczy „popraw żądanie", 5xx - „to nasza wina",
  • zasięg od najwęższego:
    @UseFilters
    na metodzie,
    @UseFilters
    na klasie,
    app.useGlobalFilters()
    globalnie,
  • @Catch
    przyjmuje wiele typów; wewnątrz rozróżniasz je przez
    instanceof
    i mapujesz na właściwe kody HTTP,
  • filtr tłumaczy wyjątki wewnętrzne na język HTTP - nazwy Twoich klas nie powinny wychodzić na zewnątrz.

W następnej lekcji nauczysz się tworzyć własne wyjątki, żeby mieć co przechwytywać - z czytelną hierarchią zamiast garści

throw new Error
. A na razie zapamiętaj: filtr to urzędnik przepisujący meldunek na jednolity formularz - klient dostaje kod i komunikat, a budowa obozu zostaje w obozie.

Przejdź do CodeWorlds