Interceptor и идентификатор запроса
При отказе клиенту полезно иметь идентификатор операции, который можно сопоставить с серверной записью. Добавим RequestTrace interceptor. Он создаёт requestId, устанавливает X-Request-Id и наблюдает завершение handler через RxJS finalize. Результат — сквозная диагностическая связь без изменения формы TicketList или Ticket. Мы также разберём ранние отказы guards, поскольку не вся серверная цепочка обязательно достигает interceptor.
RequestId отвечает на вопрос «какая операция», а actorId — «какая удостоверенная сторона». Их нельзя смешивать. Один actor может отправить несколько запросов, а один запрос записи создаёт одно предметное действие с текущей version. В лаборатории requestId генерируется randomUUID; входное пользовательское значение не принимается как авторитетное. Такой выбор упрощает договор и не требует проверять формат header, который внешний клиент мог специально подменить для диагностики.
Обёртка над handler
Ниже полный RequestTrace из составного src/http.ts. Импорты Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger, finalize и randomUUID находятся в полном файле. ApiErrors расположен рядом, но выполняет другую роль и будет рассмотрен отдельной главой. В AppModule interceptor зарегистрирован через APP_INTERCEPTOR с useClass: так контейнер создаёт глобальный provider. Он применяется к HTTP-операциям приложения, а не вручную вызывается каждым controller.
@Injectable()
export class RequestTrace implements NestInterceptor {
private readonly logger = new Logger(RequestTrace.name);
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest<ActorRequest>();
const res = context.switchToHttp().getResponse<Response>();
req.requestId ??= randomUUID(); res.setHeader('X-Request-Id',req.requestId);
return next.handle().pipe(finalize(() => this.logger.log(`${req.requestId} ${req.method} ${req.path}`)));
}
}
Intercept получает HTTP request/response, задаёт id и возвращает next.handle().pipe(finalize(...)). Возвращение Observable сохраняет обычный результат controller. Мы не преобразуем Ticket в {data:Ticket} и не добавляем items на другой уровень. Angular уже проверяет wire-модель, поэтому самовольная обёртка разрушила бы согласование двух курсов. Диагностическая обязанность должна иметь небольшой явно описанный эффект: header и разрешённая строка журнала, а не скрытая версия API-response.
Finalize вызывается при завершении наблюдаемого потока, включая error и unsubscribe. Лог содержит requestId, метод и path. Здесь не записаны body, Authorization, DATABASE_URL и текст title. Это ограничение полезно даже в лаборатории: диагностика не требует копировать всё содержимое запроса. Мы не называем такой лог полноценным audit trail. Предметная история ticket_events хранится в транзакции и имеет другую цель; строка trace не доказывает успешный COMMIT.
Что означает журнал
Наличие trace-строки означает, что наблюдаемая цепочка завершилась, а не что ответ был успешным. Код не пишет statusCode или длительность, поэтому по одной строке нельзя обещать 200 или вычислять latency. Измерения в сессии не выполнялись. Для будущей observability можно отдельно определить формат, включить статус и часы, не сохраняя чувствительные данные. Но нельзя объявлять текущие строки метриками производительности просто потому, что они находятся около обработчика запроса.
Ожидаемый GET, дошедший до controller, получает X-Request-Id. Если repository выбросит 404 или unexpected error, общий filter использует уже назначенный requestId в JSON. Тогда header и envelope можно сопоставить. На клиенте лаборатории errorMessage строится по status и не показывает id прямо в UI. При развитии службы поддержки этот id мог бы стать копируемой диагностической деталью; решение требует продумать понятную формулировку, а не выводить весь серверный stack trace.
Guards работают до interceptor. Если DemoAuthGuard отклонит неизвестный token, RequestTrace может вообще не назначить id. Поэтому ApiErrors имеет fallback randomUUID и самостоятельно ставит header для раннего отказа. Это важная деталь сквозной цепочки: нельзя утверждать, что interceptor единолично покрывает любой ответ приложения. Совместный договор обеспечивается двумя небольшими участками с ясной причиной. У error filter есть обязанность сформировать допустимую ошибку даже когда handler ещё не начался.
Регистрация и зависимости
APP_INTERCEPTOR является специальным ключом Nest для глобальной регистрации. UseClass позволяет container создать RequestTrace как provider, сохраняя возможность внедрять зависимости позднее. Регистрация через произвольный new в main была бы иной формой создания. В нашем примере logger создаётся полем класса и дополнительных зависимостей нет, но единая module-форма согласована с APP_FILTER. Это помогает находить cross-cutting средства в одном месте, а не в каждом controller.
Interceptor не выдаёт роли и не нормализует DTO. Guards допускают запрос, pipe проверяет значения, interceptor наблюдает handler, filter формирует отказ. Такое распределение описывает разные задачи, хотя все они связаны с HTTP. Если начать выполнять authorization в trace только потому, что там есть request, ответственность станет скрытой: изменение логирования сможет случайно повлиять на доступ. Текущая граница оставляет диагностическую операцию независимой от содержания заявки и разрешения PATCH.
Строка req.path не включает полный query, поэтому фильтр q не дублируется в журнале. Это упрощает чтение и не хранит пользовательский поисковый текст. Для подробного расследования иногда нужны другие поля, но их надо выбирать по назначению. Trace не является автоматическим снимком всех данных. Для учебного списка достаточно увидеть метод, маршрут и requestId, затем сопоставить ответ с ожидаемым status. Предметный title останется в базе и истории согласно отдельному договору.
Представьте два одновременных GET от одного viewer. У них один actor.id, но разные requestId. Теперь PATCH от dispatcher получил 409: id этой операции поможет отличить её от предшествующего успешного чтения. Однако сам конфликт объясняется version, а не id трассы. Даже идеально уникальные requestId не предотвращают потерю обновлений и не дают idempotency. Для этих задач нужны отдельные механизмы, которые появятся в транзакционной главе или останутся границами архива.
Фактический вывод Logger и заголовки не получены запуском. В тексте указан ожидаемый эффект написанного interceptor и fallback filter. На следующем шаге введём pg Pool: он станет реальным ресурсом приложения с отдельным завершением, а trace останется способом связать внешнюю операцию с внутренним отказом. При этом подключение базы не должно добавлять в журнал её credentials или превращать сообщение пользователю в SQL-диагностику.
Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.
Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.