Angular
Функциональные интерсепторы
В крупном Angular-приложении каждый второй запрос должен нести токен авторизации, а каждый первый - корректно обработать 401 и сетевой сбой. Раскидать это по сотне сервисов означает сто мест, где можно забыть заголовок. Команда Angular ответила функциональными интерсепторами: одна небольшая функция перехватывает весь HTTP-трафик приложения, добавляет заголовки, повторяет упавшие запросы и логирует ошибки в одной точке.
- Auth-токены: единый interceptor подставляет Authorization: Bearer во все исходящие запросы, кроме публичных
- Глобальный retry: транзиентные 503 и сетевые таймауты повторяются автоматически с backoff
- Нормализация ошибок: 401 ведёт на повторный логин, 500 показывает тост, всё в одном месте
- Корреляция запросов: X-Request-Id добавляется в каждый запрос для трассировки в Grafana и Sentry
- Loading-индикатор: счётчик активных запросов поднимается и опускается через interceptor
Предварительные знания
- Понимание HttpClient и того, как он возвращает Observable ответа
- Базовое знание RxJS-операторов: pipe, catchError, retry, throwError
- Знакомство с inject() и функциональным DI в Angular
От классов к функциям перехвата
До Angular 15 интерсептор был классом, который реализовывал интерфейс HttpInterceptor и регистрировался через мультипровайдер HTTP_INTERCEPTORS. Это требовало декоратора, конструктора с зависимостями и многословной регистрации. В Angular 15 (ноябрь 2022) появились функциональные интерсепторы: тип HttpInterceptorFn - это просто функция (req, next) с доступом к inject(). К Angular 17-21 функциональный стиль стал рекомендованным по умолчанию: меньше шаблонного кода, чистый tree-shaking и единая модель с остальным standalone-API.
Что такое HttpInterceptorFn
Функциональный интерсептор - это функция с сигнатурой HttpInterceptorFn. Она принимает входящий запрос и функцию next, которая передаёт запрос дальше по цепочке (к следующему интерсептору или к самому HttpBackend). Функция обязана вернуть Observable события HTTP. Самый простой interceptor просто пропускает запрос без изменений.
Запрос HttpRequest иммутабелен. Любое изменение - добавление заголовка, смена URL, установка тела - делается через req.clone(), который возвращает новую копию с изменёнными полями. Прямая мутация req не сработает и сломает предположения остальных интерсепторов в цепочке.
next(req) - это не конец, а продолжение цепочки. Несколько интерсепторов выстраиваются в конвейер: каждый получает запрос, может его изменить, вызвать next и обработать ответ на обратном пути. Порядок задаётся массивом при регистрации.
Почему заголовок нельзя добавить мутацией req.headers, а нужен req.clone()?
Auth-заголовки и inject() внутри интерсептора
Тело функционального интерсептора выполняется в инъекционном контексте, поэтому inject() доступен прямо внутри. Это убирает конструктор: токен-сервис, роутер или конфигурация получаются вызовом inject(Token). Типичная задача - подставить Authorization: Bearer во все запросы к собственному API.
Опция setHeaders в clone добавляет или заменяет указанные заголовки, не трогая остальные. Публичные эндпоинты (логин, регистрация, открытые ресурсы) исключаются проверкой URL, чтобы не слать токен туда, где он не нужен и может утечь в логи стороннего сервиса.
inject() внутри interceptor работает только в синхронной части функции, до подписки на Observable. Вызов inject() внутри колбэка catchError или tap уже вне инъекционного контекста и выбросит ошибку. Все зависимости берутся в начале функции.
Если токен живёт в сигнале (AuthStore с signal), читать его лучше внутри тела интерсептора при каждом запросе, а не сохранять однажды в переменную модуля. Так каждый запрос получает актуальный токен после рефреша.
Где в функциональном интерсепторе допустимо вызывать inject()?
Обработка ошибок, retry и регистрация
Обработка ответа навешивается через pipe на результат next(req). Оператор retry повторяет упавший запрос заданное число раз с задержкой, а catchError ловит ошибку, которая дошла до конца цепочки. Типичная стратегия: повторить транзиентные сбои, а на 401 отправить на повторную авторизацию.
В конфигурации retry колбэк delay решает, повторять ли попытку: для 5xx возвращается timer с растущей задержкой (примитивный backoff), для остального возвращается throwError, что немедленно прекращает повторы. catchError пробрасывает ошибку дальше через throwError, чтобы вызывающий код тоже мог отреагировать.
Порядок в массиве withInterceptors определяет порядок выполнения для исходящего запроса слева направо. На обратном пути (обработка ответа) порядок обратный. Поэтому authInterceptor, который подставляет токен, ставится раньше логирующего, чтобы лог уже видел финальный запрос.
Несколько интерсепторов переданы в withInterceptors([a, b, c]). В каком порядке они обрабатывают исходящий запрос?
Связь с другими темами
Интерсепторы стоят между HttpClient и остальным приложением:
- HttpClient — Источник запросов, которые перехватывает interceptor. Без понимания клиента нет смысла в перехвате
- Валидация форм — Серверные ошибки приходят по HTTP, и interceptor приводит их к единому формату до показа в форме
Итог
- HttpInterceptorFn - это функция (req, next) => Observable, заменяющая старый класс HttpInterceptor
- Запросы иммутабельны: для изменения заголовков используется req.clone(), а не мутация
- inject() работает внутри interceptor, так что зависимости (токен-сервис, роутер) получаются без конструктора
- Обработка ошибок и retry навешиваются через pipe на результат next(req): catchError и retry
- Регистрация - один вызов provideHttpClient(withInterceptors([...])), порядок в массиве задаёт порядок выполнения
Связанные уроки
- ng-26-httpclient — Интерсептор перехватывает запросы HttpClient, поэтому сначала нужно понимать как устроен сам клиент
- ng-32-form-validation — Серверные ошибки валидации приходят через HTTP, и единый interceptor нормализует их перед показом в форме