Перейти до основного вмісту

HTTP Інтерсептори

Інтерсептори дуже близькі по функціональності до контролерів, але вони не створюють роутів, вони прив'язуються до вже існуючих роутів. На одному роуті може працювати ціла група інтерсепторів, що запускаються один за одним. Інтерсептори - це аналог middleware в ExpressJS, але інтерсептори можуть використовувати DI. Окрім цього, інтерсептори можуть працювати до та після роботи контролера.

Враховуючи що інтерсептори роблять таку ж роботу, яку можуть робити контролери, без інтерсепторів можна обійтись. Але в такому разі вам прийдеться значно частіше викликати різні сервіси в контролерах.

Як правило, інтерсептори використовують для автоматизації стандартної обробки, такої як:

  • парсинг тіла запиту чи заголовків;
  • валідація запиту;
  • збирання та логування різних метрик роботи застосунку;
  • кешування;
  • і т.д.

Інтерсептори можна централізовано підключати або відключати, не змінюючи при цьому код методів контролерів, до яких вони прив'язуються. Як і контролери, інтерсептори можуть бути одинаками чи неодинаками. На відміну від одинаків, неодинаки мають доступ до інжектора на рівні запиту, тому вони можуть викликати сервіси на рівні запиту. З іншого боку, одинаки створюються на рівні роуту, відповідно - для них доступні сервіси на рівні роуту, модуля чи застосунку.

Схема обробки HTTP-запиту

Неодинак

Обробка HTTP-запиту має наступний робочий потік:

  1. Ditsmod створює інстанс PreRouter на рівні застосунку.
  2. PreRouter за допомогою роутера шукає обробника запиту відповідно до URI.
  3. Якщо обробника запиту не знайдено, PreRouter видає помилку зі статусом 501.
  4. Якщо знайшовся обробник запиту, Ditsmod створює інстанс провайдера з токеном HttpFrontend на рівні запиту, ставить його першим у черзі інтерсепторів і автоматично викликає. By default, цей інтерсептор відповідає за встановлення значень для провайдерів з токенами QUERY_PARAMS та PATH_PARAMS.
  5. Якщо в поточному маршруті є ґарди, то по-дефолту запускається InterceptorWithGuards зразу після HttpFrontend.
  6. Наступними можуть запуститись інші інтерсептори, це залежать від того, чи запустить їх попередній у черзі інтерсептор.
  7. Якщо усі інтерсептори відпрацювали, Ditsmod запускає HttpBackend, інстанс якого створюється на рівні запиту. By default, HttpBackend запускає безпосередньо метод контролера, що відповідає за обробку поточного запиту.

Отже, приблизний порядок обробки запиту такий:

  1. PreRouter;
  2. HttpFrontend;
  3. InterceptorWithGuards;
  4. інші інтерсептори;
  5. HttpBackend;
  6. метод контролера.

Враховуючи, що починаючи від PreRouter і до методу контролера передається проміс, то цей же проміс резолвиться у зворотньому порядку - від методу контролера до PreRouter.

Оскільки інстанси PreRouter, HttpFrontend, InterceptorWithGuards та HttpBackend створюються за допомогою DI, ви можете їх підміняти своєю версією відповідних класів. Наприклад, якщо ви хочете не просто відправити 501-ий статус у випадку відсутності потрібного роута, а хочете ще й додати певний текст чи змінити заголовки, ви можете підмінити PreRouter своїм класом.

Кожен виклик інтерсептора повертає Promise<any>, і в кінцевому підсумку він приводить до метода контролера, прив'язаного до відповідного роута. Це означає, що в інтерсепторі ви можете слухати результат резолву проміса, що повертає метод контролера. Щоправда, на даний момент (Ditsmod v2.0.0), HttpFrontend та HttpBackend by default ігнорують усе, що повертає контролер чи інтерсептори, тому прослуховування резолву проміса може бути корисним хіба що для збору метрик.

З іншого боку, через DI ви легко можете підмінити HttpFrontend або HttpBackend своїми власними інтерсепторами, щоб брати до уваги значення, що повертає метод контролера. Один із варіантів такої функціональності реалізовано у модулі @ditsmod/return.

Одинак

Інтерсептор-одинак працює дуже подібним чином до неодинака, але при цьому він не використовує інжектор на рівні запиту. Робочий потік за його участі відрізняється у пункті 4 та 7, оскільки інстанс інтерсептора-одинака створюється на рівні роуту:

  1. Ditsmod створює інстанс PreRouter на рівні застосунку.
  2. PreRouter за допомогою роутера шукає обробника запиту відповідно до URI.
  3. Якщо обробника запиту не знайдено, PreRouter видає помилку зі статусом 501.
  4. Якщо знайшовся обробник запиту, Ditsmod використовує інстанс провайдера з токеном HttpFrontend на рівні роуту, ставить його першим у черзі інтерсепторів і автоматично викликає. By default, цей інтерсептор відповідає за встановлення значень pathParams та queryParams для SingletonRequestContext.
  5. Якщо в поточному маршруті є ґарди, то по-дефолту запускається SingletonInterceptorWithGuards зразу після HttpFrontend.
  6. Наступними можуть запуститись інші інтерсептори, це залежать від того, чи запустить їх попередній у черзі інтерсептор.
  7. Якщо усі інтерсептори відпрацювали, Ditsmod запускає HttpBackend, інстанс якого використовується на рівні роуту. By default, HttpBackend запускає безпосередньо метод контролера, що відповідає за обробку поточного запиту.

Створення інтерсептора

Кожен інтерсептор повинен бути класом, що впроваджує інтерфейс HttpInterceptor, та має анотацію з декоратором injectable:

import { injectable, RequestContext, HttpHandler, HttpInterceptor } from '@ditsmod/core';

@injectable()
export class MyHttpInterceptor implements HttpInterceptor {
intercept(next: HttpHandler, ctx: RequestContext) {
return next.handle(); // Here returns Promise<any>;
}
}

Як бачите, метод intercept() має два параметри: у перший передається інстанс обробника, що викликає наступний інтерсептор, а у другий - RequestContext (нативні об'єкти запиту та відповіді від Node.js). Якщо для своєї роботи інтерсептор потребує додаткових даних, їх можна отримати в конструкторі через DI, як і в будь-якому сервісі.

Передача інтерсептора в інжектор

Інтерсептор неодинак передається в інжектор на рівні запиту за допомогою мульти-провайдерів з токеном HTTP_INTERCEPTORS:

import { HTTP_INTERCEPTORS, featureModule } from '@ditsmod/core';
import { MyHttpInterceptor } from './my-http-interceptor.js';

@featureModule({
// ...
providersPerReq: [{ token: HTTP_INTERCEPTORS, useClass: MyHttpInterceptor, multi: true }],
})
export class SomeModule {}

Передача інтерсептора-одинака відбувається точно таким же чином, але на рівні роуту, модуля чи застосунку:

import { HTTP_INTERCEPTORS, featureModule } from '@ditsmod/core';
import { MyHttpInterceptor } from './my-http-interceptor.js';

@featureModule({
// ...
providersPerApp: [{ token: HTTP_INTERCEPTORS, useClass: MyHttpInterceptor, multi: true }],
})
export class SomeModule {}

В даному разі інтерсептор передано на рівні застосунку, але майте на увазі, що якщо ви передасте також інтерсептори на нижчих рівнях, то даний інтерсептор буде ігноруватись. Саме так працюють мульти-провайдери.

В даному разі інтерсептори передаються в метадані модуля. Так само вони можуть передаватись у метадані контролера. Тобто інтерсептори можуть працювати або для усіх контролерів у модулі без виключень, або тільки для конкретного контролера. Якщо інтерсептори потрібно додати лише до окремих роутів у межах контролерів, це ви можете зробити за допомогою розширень (таким чином додаються інтерсептори для парсингу тіла запиту).