NestJS / 38 минут

Interceptors и RxJS

Оборачивайте обработку запроса и преобразуйте Observable результата.

В процессе Теория: впереди Практика: впереди Тест: впереди
09 / 14
После этой главы
  • Оборачивайте обработку запроса и преобразуйте Observable результата.
  • Применить тему в задаче «Interceptors и RxJS» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Обёртка вокруг обработки

Interceptor получает управление до вызова обработчика и может преобразовать результат после него. Это удобно для общего конверта ответа, измерения времени, логирования завершения или согласованного преобразования значений. Guard отвечает за допуск, pipe проверяет аргумент, filter обрабатывает исключение. Выбор стадии делает обязанности кода понятными.

Интерцептор реализует NestInterceptor. В intercept передаются ExecutionContext и CallHandler. Вызов next.handle возвращает Observable, представляющий дальнейшее выполнение цепочки. Верните этот поток Nest, чтобы framework управлял подпиской и отправкой ответа. Самостоятельный subscribe обычно отделяет работу от основной цепочки и ломает ожидание результата и ошибок.

TypeScript
@Injectable()
class EnvelopeInterceptor implements NestInterceptor {
  intercept(_context: ExecutionContext, next: CallHandler) {
    return next.handle().pipe(map(value => ({ data: value })));
  }
}

Поток и операторы

Observable описывает последовательность значений, ошибок и завершения. В обычном HTTP handler результат часто один, но модель потока позволяет единообразно работать с асинхронными этапами. map преобразует каждое значение в новое. Он не должен неожиданно выполнять дополнительную запись в БД ради формирования JSON. Побочные действия выделяйте явно и оценивайте их жизненный цикл.

TypeScript
import { map } from 'rxjs/operators';
const source = of({ title: 'JS' });
const wrapped = source.pipe(map(book => ({ data: book })));
// При подписке результат: { data: { title: 'JS' } }

Пример использует of из rxjs. pipe здесь — метод композиции операторов RxJS, а не Nest PipeTransform. Совпадающее слово не означает одинаковую стадию. Успешный HTTP-ответ и ошибка — разные каналы потока: map не превращает исключение автоматически в объект data с ошибкой внутри.

Измерение и завершение

Для измерения длительности сохраните стартовое время в локальной переменной intercept. finalize выполняет завершающее действие при окончании или прекращении подписки, включая ошибочную ветвь. Не храните время текущего запроса в поле singleton-интерцептора: параллельные запросы перезапишут его и дадут неверные измерения.

TypeScript
intercept(_context: ExecutionContext, next: CallHandler) {
  const started = performance.now();
  return next.handle().pipe(finalize(() => {
    this.metrics.observe(performance.now() - started);
  }));
}

metrics в примере — внедрённый безопасный адаптер. Логирование всего request или response может раскрыть cookie, пароль и пользовательские данные. Для метрик обычно достаточно имени маршрута, категории статуса и длительности. Не используйте сырой URL с идентификаторами как неограниченную метку: это создаёт слишком много рядов статистики.

Ошибки и порядок обёрток

catchError позволяет преобразовать ошибку или повторно выбросить её через throwError. Это полезно для конкретного преобразования, но не должно делать любой сбой успешным HTTP 200. Если нужен общий внешний формат исключений, удобнее exception filter. Таймаут потока тоже не гарантирует, что сторонняя библиотека прекратила сетевой запрос: для этого требуется поддержка отмены самим клиентом.

Interceptors оборачивают друг друга. Входная часть глобального выполняется до уровня контроллера и маршрута, а результат проходит обратный путь. Если два интерцептора оборачивают ответ, порядок влияет на форму JSON. Проверьте итоговый контракт через HTTP, особенно при сочетании сериализации класса и общего конверта.

Регистрация и ограничения

UseInterceptors назначает interceptor обработчику или классу. Глобальная регистрация через APP_INTERCEPTOR позволяет контейнеру внедрять зависимости. Ручное создание экземпляра в bootstrap подходит для простого объекта, но не разрешает его конструктор через DI автоматически. Область должна соответствовать задаче: не все скачивания файлов нуждаются в JSON-конверте.

TypeScript
@Get('value')
@UseInterceptors(EnvelopeInterceptor)
getValue() { return 'JavaScript'; }

Нативное управление Res с ручной отправкой может обойти стандартное преобразование результата. Не оборачивайте поток файла или уже отправленный ответ как обычный объект. Контракт разных типов маршрутов следует описать явно. Также конверт не удаляет приватные поля: содержимое data всё равно должно быть безопасным публичным DTO.

Практика конверта

Создайте interceptor с RxJS map, который превращает успешный результат в объект с единственным полем data. GET /value возвращает переданный text. solve делает реальный HTTP-запрос, возвращает body и закрывает приложение. Пустая строка должна сохраниться как data: "", а не превратиться в отсутствие результата из-за проверки на truthy.

После решения исследуйте объект вместо строки и исключение вместо успешного значения. Объясните, почему только успешный ответ проходит через map. В проекте библиотеки добавьте отдельный тест на внешний JSON-контракт и измерение длительности, не затрагивая поля с приватными данными и не создавая ручной подписки внутри intercept.

Загружаем редактор…
Следующая темаИсключения и filters

Музыка: Reverie — Scott Buckley · CC BY 4.0