NestJS 拦截器
目录
拦截器
拦截器(Interceptors)是 NestJS 中一个强大的功能,它就像是一个"中间人",可以在请求到达控制器之前和响应发送给客户端之前进行拦截和处理。通过 @Injectable()
装饰器注解并实现 NestInterceptor
接口来创建。
拦截器的灵感来自于 AOP(面向切面编程)的概念,它允许你在不修改应用程序代码的情况下,添加横切关注点(cross-cutting concerns),如日志记录、性能分析、权限验证等。
拦截器可以实现以下功能:
在方法执行前后添加额外逻辑 - 可以在请求处理前后注入自定义的业务逻辑,如日志记录、请求验证等
转换函数返回结果 - 可以修改或转换控制器返回的响应数据,确保返回统一的数据格式
异常转换处理 - 可以捕获并转换函数执行过程中抛出的异常,实现统一的异常处理
扩展核心功能 - 可以为现有功能添加额外的行为,如添加响应头、修改状态码等
条件函数覆盖 - 可以根据特定条件决定是否执行原始函数,常用于实现缓存、权限控制等场景
这些功能使得拦截器成为实现横切关注点的理想工具,能够以非侵入式的方式增强应用程序的功能。
基本
每个拦截器都需要实现 intercept()
方法,该方法接收两个重要参数:
ExecutionContext
实例,这是与守卫(guards)中使用的完全相同的对象。它继承自ArgumentsHost
类,提供了访问请求、响应等底层对象的能力。ArgumentsHost
还根据不同的应用类型(如 HTTP、RPC、WebSocket)暴露不同的参数数组。例如,在 HTTP 场景下,你可以通过context.switchToHttp().getRequest()
获取原生的请求对象,通过context.switchToHttp().getResponse()
获取响应对象;在 RPC 或 WebSocket 场景下,则有对应的switchToRpc()
、switchToWs()
方法。这样可以让拦截器在不同协议下灵活地访问和操作底层数据。CallHandler
实例,这个接口实现了handle()
方法,用于调用路由处理程序。如果不在intercept()
方法中调用handle()
,路由处理程序就不会被执行。
ExecutionContext
的继承关系值得注意:
ExecutionContext
继承自ArgumentsHost
,而ArgumentsHost
是一个通用的参数持有者,能够根据当前上下文(HTTP、RPC、WebSocket)暴露不同的参数数组。- 具体来说,
ExecutionContext
在ArgumentsHost
的基础上,增加了对当前处理器(handler)、控制器(class)等元数据的访问能力,这对于拦截器、守卫等高级用法非常有用。 - 你可以通过
context.getHandler()
获取当前被调用的方法(处理器),通过context.getClass()
获取当前控制器类,这对于实现基于元数据的逻辑(如自定义装饰器、权限控制等)非常关键。
执行上下文
通过扩展 ArgumentsHost
,ExecutionContext
添加了几个重要的辅助方法,这些方法提供了关于当前执行过程的详细信息:
getClass() - 返回当前处理请求的控制器类的类型。这对于需要基于控制器类型进行不同处理的场景非常有用。
getHandler() - 返回对将要调用的处理程序(路由处理方法)的引用。这让你能够访问方法的元数据,比如通过装饰器添加的自定义元数据。
这些方法使得拦截器能够:
- 访问元数据 - 结合自定义装饰器,可以读取控制器或方法级别的元数据,实现更灵活的逻辑控制
- 动态处理 - 根据不同的控制器类型或处理方法采取不同的处理策略
- 条件执行 - 基于方法特征决定是否执行某些逻辑,如缓存、日志等
- 权限控制 - 结合角色装饰器,实现细粒度的权限验证
示例:通过 getHandler()
获取路由方法的缓存配置:
import { Injectable, CacheInterceptor, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
constructor(protected readonly reflector: Reflector) {
super();
}
trackBy(context: ExecutionContext): string | undefined {
// 获取当前处理的 handler(方法)
const handler = context.getHandler();
// 获取自定义的缓存 key(假设你用 @SetCacheKey() 装饰器设置了元数据)
const cacheKey = this.reflector.get<string>('cache_key', handler);
// 如果缓存 key 存在,则返回缓存 key
if (cacheKey) {
return cacheKey;
}
// 默认行为
return super.trackBy(context);
}
}
@SetCacheKey
装饰器实现
import { SetMetadata } from '@nestjs/common';
/**
* 设置缓存 key 的自定义装饰器
* @param key 缓存 key
*/
export const SetCacheKey = (key: string) => SetMetadata('cache_key', key);
trackBy()
方法 是用来生成当前请求的“缓存键”的
return super.trackBy(context)
是走父类的缓存键生成规则。如果你没有设置自定义的缓存 Key,那么就使用默认的规则去生成缓存 Key。
NestJS 的缓存系统底层是一个“键值对”机制(类似 Redis)。
每个请求,都要生成一个唯一的缓存键(Key),才能对响应数据进行缓存。
return cache.get(key); // key 用于查缓存
如果使用了@SetCacheKey('xxx')
装饰器 → 用你设置的 key
否则 → 走父类的 trackBy 方法,用默认机制生成 key
更多关于缓存机制,请阅读:Nest中的请求缓存机制
调用处理程序
CallHandler
接口实现了 handle()
方法,这个方法在拦截器中扮演着关键角色。它的主要作用是调用路由处理程序方法。需要注意的是,如果在 intercept()
方法中没有调用 handle()
,路由处理程序将不会被执行。
这种设计使得 intercept()
方法能够完全控制请求/响应流程:
- 你可以在调用
handle()
之前执行前置逻辑 - 由于
handle()
返回 Observable,你可以使用 RxJS 操作符处理响应流 - 这种设计符合面向切面编程(AOP)理念,其中路由处理程序的调用点被称为"切入点"
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, map, catchError } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('拦截器:请求进入');
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`拦截器:请求处理完毕,用时 ${Date.now() - now}ms`)),
map(data => {
console.log('拦截器:可以在这里修改响应数据');
return data; // 可以在这里加工/包装响应数据
}),
catchError(err => {
console.error('拦截器:捕获到异常', err);
throw err; // 重新抛出让框架处理
})
);
}
}
next.handle()
触发路由处理程序的执行(即你的控制器方法真正开始执行)。
它返回一个 Observable(可观察的数据流),表示“处理程序的结果(数据)”。
pipe()
处理的是从处理程序返回的数据流,在处理程序结果生成后,在数据返回客户端前,插入额外的加工/处理流程。
作用:把多个 RxJS 操作符串联在一起,形成“流水线”式的数据处理流程。
类比:就像加工流水线一样,数据会顺序通过这些“加工环节”(操作符)进行处理。
return next.handle().pipe(
// 各种“加工步骤”在这里
);
Nest 的路由处理方法返回的响应是 Observable(可观察的数据流),需要用 pipe
把后续逻辑附加上去,否则无法在数据返回前做任何拦截、加工或处理。
tap()
作用:执行副作用操作(比如日志记录),但不改变数据。
类比:像“旁观者”,只是看着数据流过,顺便打印或记录一些信息。
tap(() => console.log('响应数据准备返回'))
记录日志、统计耗时、触发某些非核心业务逻辑(比如通知等)。
map()
作用:修改/变换数据,类似数组的 map
方法。
类比:像“加工环节”,把原材料变成成品。
map(data => {
// 修改响应
return { success: true, data };
})
统一包装接口返回格式、转换敏感字段、裁剪数据等。
catchError()
作用:捕获上游的错误,并做相应处理。
类比:像异常处理“护栏”,防止整个流程崩溃。
catchError(err => {
console.error('发生错误', err);
throw err;
})
统一错误日志、抛出自定义异常、做降级处理等。
handle()
:就像开启了一条“数据流水线”,你可以接管这个过程。
pipe()
:在流水线上安装“加工设备”。
tap()
:旁观流水线,记录情况。
map()
:修改流水线上的产品。
catchError()
:万一流水线出问题,做应急处理。
因为拦截器本质上是“请求 - 响应过程的拦截层”,有时需要:
- 在请求进入时预处理;
- 在响应返回前加工数据;
- 在流程出错时捕捉异常。
请求进入
↓
前置逻辑 (intercept 前半部分)
↓
next.handle() ← 路由处理程序开始工作
↓
数据流返回 (Observable)
↓
.pipe( ← 数据加工阶段
tap() ← 旁观记录
map() ← 修改包装
catchError() ← 错误处理
)
↓
返回数据给客户端
next.handle():开始执行核心业务逻辑(路由处理程序),得到一个“数据流”。
pipe:拿到这个“数据流”后,插入自定义逻辑,对即将返回的数据进行加工。
切面拦截
在 NestJS 中,拦截器(Interceptor)是实现面向切面编程(AOP)的重要机制之一。它允许我们在请求进入路由处理程序之前和响应返回客户端之前,插入自定义的逻辑。
一个典型的使用场景是:记录用户操作,如统计接口耗时、记录访问日志、触发异步事件等。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle() // 执行路由处理程序(核心业务逻辑)
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)) // 记录耗时
);
}
}
NestInterceptor<T, R>:
泛型接口:
T
:请求通过数据流传递时的类型。R
:响应返回的数据类型。
由于 handle()
返回的是 RxJS 的 Observable
对象,我们可以利用 RxJS 提供的丰富操作符来处理和转换这个数据流。在上述示例中,我们使用了 tap()
操作符,它的特点是可以让我们"窥视"数据流,执行一些副作用操作(如记录日志),但不会改变数据流本身。无论数据流是正常结束还是发生异常,我们注册的匿名日志函数都会被调用,同时又不会干扰到正常的响应流程。
绑定拦截器
为了设置拦截器,我们可以使用 @UseInterceptors
,它从 @nestjs/common 导入,和管道和守卫一样,拦截器可以是全局作用域,控制器作用域,方法作用域。
当一个控制器使用 @UseInterceptors
装饰器时,拦截器会应用到控制器中的所有路由。
使用 @UseInterceptors
装饰器通常传递一个类,将实例化的责任交给框架,也可以传递一个就地实例。就地实例通常用于传入参数。
当想要设置一个全局拦截器的时候,需要使用 Nest 应用实例的 useGlobalInterceptors
方法。
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
在 NestJS 中,如果你希望全局拦截器能够注入依赖(比如服务、配置等),推荐的做法是在模块内部通过依赖注入注册全局拦截器,而不是直接在 main.ts
里用 useGlobalInterceptors()
实例化。这样,拦截器就能像普通服务一样自动注入依赖。
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
**注意事项**
- 当你通过
APP_INTERCEPTOR
这种方式注册拦截器时,无论你在哪个模块(比如上例的AppModule
或其他功能模块)中这样做,这个拦截器都会成为全局拦截器,即对整个应用的所有请求生效。 - 因此,推荐在定义拦截器(如
LoggingInterceptor
)的模块中完成注册,这样结构更清晰、职责更明确。 useClass
不只是注册自定义提供者的一种方式。你还可以使用useExisting
(复用已有提供者实例)或useFactory
(通过工厂函数动态创建实例)等方式注册拦截器。
响应映射
handle()
返回的是一个 Observable
对象,该对象封装了路由处理程序返回的数据流,可以使用map
操作符来修改响应数据。
**注意事项**
响应映射功能不适用于直接操作底层响应对象(如 @Res()
)的场景。如果你在控制器方法中注入并操作了 @Res()
(比如直接调用 res.json()
或 res.send()
),拦截器的响应映射(如 map
操作符)将不会生效。建议优先采用 NestJS 推荐的响应返回方式(即直接 return 数据),避免直接依赖底层库的响应对象,这样才能充分发挥拦截器的响应增强能力。
下面我们来实现一个最常见的响应映射拦截器 —— TransformInterceptor
。它的作用是:把控制器原本返回的数据包裹到一个统一的对象结构中,比如 { data: ... }
,这样前端拿到的数据格式就始终一致,便于处理和扩展。
RxJS 的 map
操作符将响应对象分配给新创建对象的 data 属性,再将这个新对象返回给客户端。
const response = await next.handle().pipe(
map(data => ({ data }))
)
return response;
response
是一个 Observable
对象,框架将自动订阅并处理这个响应流,最终返回给客户端。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
在 Nest 拦截器中,intercept()
方法既可以是同步的,也可以是异步的。如果你的拦截逻辑中需要执行异步操作(比如访问数据库、调用外部 API 等),只需将 intercept()
方法声明为 async
,并在方法体内使用 await
即可。Nest 会自动识别并正确处理异步拦截器,无需额外配置。
上述代码执行后的响应数据结构如下:
{
"data": ...
}
注意:上述的 data
是属性的简写形式,等价于{data: data}
。
拦截器的强大之处在于它可以为全局需求提供统一、可复用的解决方案。例如,假设我们希望所有接口返回的数据中,凡是出现 null
的地方都自动转换为 ''
(空字符串),这样前端在处理数据时就不必再做额外的判空处理。
实现思路如下:
- 递归遍历响应数据:无论是对象还是数组,都需要递归地将所有值为
null
的字段替换为''
。 - 封装为拦截器:在拦截器的
intercept
方法中,利用 RxJS 的map
操作符对响应数据进行处理。 - 全局注册:将该拦截器注册为全局拦截器,确保所有接口响应都自动应用该逻辑。
下面是实现代码:
代码
简单版本:
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
完整版本:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class NullToEmptyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => this.transformNullToEmpty(data))
);
}
/*
递归遍历数据,将 null 值转换为空字符串
@param 要处理的数据
@returns 处理后的数据
*/
private transformNullToEmpty(data: any): any {
// 如果数据为 null,直接返回空字符串
if (data === null) {
return '';
}
// 如果数据为 undefined,返回空字符串
if (data === undefined) {
return '';
}
// 如果是数组,递归处理每个元素
if (Array.isArray(data)) {
return data.map(item => this.transformNullToEmpty(item));
}
// 如果是对象(但不是 Date、RegExp 等特殊对象),递归处理每个属性
if (typeof data === 'object' && data !== null) {
// 排除一些不需要递归处理的对象类型
if (data instanceof Date || data instanceof RegExp || data instanceof Error) {
return data;
}
const result: any = {};
for (const key in data) {
if (data.hasOwnProperty(key)) {
result[key] = this.transformNullToEmpty(data[key]);
}
}
return result;
}
// 其他类型的数据直接返回
return data;
}
}
全局注册方式
在 app.module.ts
中注册为全局拦截器:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { NullToEmptyInterceptor } from './interceptors/null-to-empty.interceptor';
@Module({
imports: [
// 其他模块
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: NullToEmptyInterceptor,
},
],
})
export class AppModule {}
使用效果
注册该拦截器后,所有接口的响应数据中的 null
值都会被自动转换为空字符串:
// 控制器返回的数据
@Get()
getData() {
return {
name: '张三',
age: null,
address: {
city: '北京',
street: null,
details: [null, '详细地址', null]
},
tags: [null, '标签1', null, '标签2']
};
}
转换后的响应数据:
{
"data": {
"name": "张三",
"age": "",
"address": {
"city": "北京",
"street": "",
"details": ["", "详细地址", ""]
},
"tags": ["", "标签1", "", "标签2"]
}
}
优化版本
如果你希望更精确地控制哪些字段需要转换,可以添加一个配置选项:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface NullToEmptyOptions {
// 是否转换 null 值
transformNull?: boolean;
// 是否转换 undefined 值
transformUndefined?: boolean;
// 排除的字段名(这些字段的 null 值不会被转换)
excludeFields?: string[];
}
@Injectable()
export class NullToEmptyInterceptor implements NestInterceptor {
constructor(private options: NullToEmptyOptions = {}) {
this.options = {
transformNull: true,
transformUndefined: false,
excludeFields: [],
...options
};
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => this.transformNullToEmpty(data))
);
}
private transformNullToEmpty(data: any, path: string = ''): any {
// 检查是否在排除列表中
if (this.options.excludeFields?.includes(path)) {
return data;
}
// 处理 null 值
if (data === null && this.options.transformNull) {
return '';
}
// 处理 undefined 值
if (data === undefined && this.options.transformUndefined) {
return '';
}
// 处理数组
if (Array.isArray(data)) {
return data.map((item, index) =>
this.transformNullToEmpty(item, `${path}[${index}]`)
);
}
// 处理对象
if (typeof data === 'object' && data !== null) {
if (data instanceof Date || data instanceof RegExp || data instanceof Error) {
return data;
}
const result: any = {};
for (const key in data) {
if (data.hasOwnProperty(key)) {
const currentPath = path ? `${path}.${key}` : key;
result[key] = this.transformNullToEmpty(data[key], currentPath);
}
}
return result;
}
return data;
}
}
使用优化版本:
// 在模块中注册时指定配置
{
provide: APP_INTERCEPTOR,
useClass: new NullToEmptyInterceptor({
transformNull: true,
transformUndefined: false,
excludeFields: ['sensitiveField', 'timestamp']
}),
}
这样,你就可以灵活控制哪些字段需要转换,哪些字段保持原样,使拦截器更加实用和可配置。
异常映射
异常映射是拦截器的另一个强大功能,它允许我们在请求处理过程中捕获和转换异常。通过利用 RxJS 的 catchError()
操作符,我们可以统一处理异常、记录错误日志、或者将内部异常转换为用户友好的错误信息。
基本异常转换:
最常见的用例是将系统内部异常转换为统一的外部异常格式。例如,当数据库连接失败或第三方服务不可用时,我们可能希望向客户端返回一个通用的"服务暂时不可用"错误,而不是暴露具体的技术细节。
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadGatewayException())),
);
}
}
这个简单的拦截器会捕获所有异常,并统一转换为 BadGatewayException
(502 错误)。虽然简单,但在某些场景下非常有用,比如当你的应用依赖多个外部服务,而你希望对客户端隐藏具体的错误细节时。
智能异常映射:
在实际项目中,我们通常需要更智能的异常处理策略。下面是一个更完善的异常映射拦截器:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
HttpException,
InternalServerErrorException,
BadRequestException,
NotFoundException,
Logger,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class SmartErrorsInterceptor implements NestInterceptor {
private readonly logger = new Logger(SmartErrorsInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(error => {
// 记录错误日志
this.logger.error(`异常捕获: ${error.message}`, error.stack);
// 如果已经是 HTTP 异常,直接抛出
if (error instanceof HttpException) {
return throwError(() => error);
}
// 根据错误类型进行映射
const mappedException = this.mapError(error);
return throwError(() => mappedException);
}),
);
}
private mapError(error: any): HttpException {
// 数据库相关错误
if (error.code === 'ER_DUP_ENTRY' || error.code === '23505') {
return new BadRequestException('数据已存在,请检查输入信息');
}
// 数据库连接错误
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
return new InternalServerErrorException('服务暂时不可用,请稍后重试');
}
// 验证错误
if (error.name === 'ValidationError') {
return new BadRequestException(`数据验证失败: ${error.message}`);
}
// 资源未找到错误
if (error.message?.includes('not found') || error.code === 'ENOENT') {
return new NotFoundException('请求的资源不存在');
}
// 权限相关错误
if (error.code === 'EACCES' || error.message?.includes('permission')) {
return new BadRequestException('权限不足,无法执行此操作');
}
// 默认处理:返回通用的内部服务器错误
return new InternalServerErrorException('服务器内部错误,请联系管理员');
}
}
异常日志记录:
除了转换异常,我们还可以利用拦截器来统一记录异常信息,这对于问题排查和系统监控非常重要:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@Injectable()
export class ErrorLoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(ErrorLoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body, query, params } = request;
return next.handle().pipe(
catchError(error => {
// 构建详细的错误上下文信息
const errorContext = {
timestamp: new Date().toISOString(),
method,
url,
body: JSON.stringify(body),
query: JSON.stringify(query),
params: JSON.stringify(params),
userAgent: request.headers['user-agent'],
ip: request.ip,
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
};
// 记录详细的错误日志
this.logger.error(
`API异常 [${method} ${url}]: ${error.message}`,
JSON.stringify(errorContext, null, 2),
);
// 重新抛出异常,让框架继续处理
return throwError(() => error);
}),
);
}
}
异常统计和监控:
我们还可以利用拦截器来收集异常统计信息,用于系统监控和告警:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
interface ErrorStats {
count: number;
lastOccurred: Date;
endpoints: Set<string>;
}
@Injectable()
export class ErrorStatsInterceptor implements NestInterceptor {
private errorStats = new Map<string, ErrorStats>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const endpoint = `${request.method} ${request.url}`;
return next.handle().pipe(
catchError(error => {
// 更新错误统计
this.updateErrorStats(error.constructor.name, endpoint);
// 检查是否需要触发告警
this.checkAlertThreshold(error.constructor.name);
return throwError(() => error);
}),
);
}
private updateErrorStats(errorType: string, endpoint: string): void {
if (!this.errorStats.has(errorType)) {
this.errorStats.set(errorType, {
count: 0,
lastOccurred: new Date(),
endpoints: new Set(),
});
}
const stats = this.errorStats.get(errorType)!;
stats.count++;
stats.lastOccurred = new Date();
stats.endpoints.add(endpoint);
}
private checkAlertThreshold(errorType: string): void {
const stats = this.errorStats.get(errorType);
if (stats && stats.count > 10) { // 阈值:10次
console.warn(`⚠️ 异常告警: ${errorType} 已发生 ${stats.count} 次`);
// 这里可以集成实际的告警系统,如发送邮件、短信等
}
}
// 提供获取统计信息的方法
getErrorStats(): Map<string, ErrorStats> {
return new Map(this.errorStats);
}
// 重置统计信息
resetStats(): void {
this.errorStats.clear();
}
}
组合使用多个异常拦截器:
在实际项目中,我们可能需要同时使用多个异常处理拦截器。NestJS 支持拦截器的链式调用,执行顺序遵循"洋葱模型":
// 在控制器或全局级别组合使用
@UseInterceptors(
ErrorLoggingInterceptor, // 先执行日志记录
ErrorStatsInterceptor, // 然后统计异常
SmartErrorsInterceptor, // 最后进行异常转换
)
@Controller('api')
export class ApiController {
// 控制器方法...
}
或者在全局级别注册:
// app.module.ts
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ErrorLoggingInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: ErrorStatsInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: SmartErrorsInterceptor,
},
],
})
export class AppModule {}
最佳实践
保持异常信息的一致性:确保转换后的异常信息对客户端友好,同时保留足够的调试信息。
避免信息泄露:不要在生产环境中向客户端暴露敏感的系统内部信息。
合理使用日志级别:根据异常的严重程度选择合适的日志级别(error、warn、info)。
性能考虑:异常处理逻辑应该尽可能轻量,避免在异常处理过程中引入新的性能问题。
监控和告警:建立完善的异常监控机制,及时发现和处理系统问题。
通过合理使用异常映射拦截器,可以构建一个健壮、用户友好且易于维护的错误处理系统。
流覆盖
在某些场景下,我们可能需要完全阻止调用路由处理程序,并直接返回一个预设的响应。这种技术被称为"流覆盖"(Flow Override),它允许拦截器完全接管请求的处理流程。最典型的应用场景包括缓存机制、请求限流、条件性响应等。
基本概念:
流覆盖的核心思想是:在拦截器中根据特定条件决定是否需要执行原始的路由处理程序。如果条件满足(比如缓存命中),就直接返回预设的响应;否则,继续执行正常的处理流程。
这种机制的关键在于:
- 使用 RxJS 的
of()
操作符创建一个新的 Observable 流 - 不调用
next.handle()
,从而跳过路由处理程序的执行 - 直接返回我们想要的响应数据
缓存拦截器实现:
让我们从一个简单的缓存示例开始,然后逐步完善它:
基础版本:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]); // 直接返回缓存数据,不执行路由处理程序
}
return next.handle(); // 执行正常的处理流程
}
}
上述示例中的 isCached
是硬编码的,of([])
返回一个包含空数组的 Observable。当 isCached
为 true
时,路由处理程序根本不会被调用,客户端会立即收到空数组响应。
实用版本:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class SmartCacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
private readonly TTL = 60000; // 缓存有效期:60秒
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const cacheKey = this.generateCacheKey(request);
// 检查缓存
const cachedData = this.getFromCache(cacheKey);
if (cachedData) {
console.log(`缓存命中: ${cacheKey}`);
return of(cachedData); // 返回缓存数据
}
// 缓存未命中,执行正常流程并缓存结果
console.log(`缓存未命中: ${cacheKey}`);
return next.handle().pipe(
tap(response => {
this.setCache(cacheKey, response);
console.log(`数据已缓存: ${cacheKey}`);
})
);
}
private generateCacheKey(request: any): string {
const { method, url, query } = request;
return `${method}:${url}:${JSON.stringify(query)}`;
}
private getFromCache(key: string): any {
const cached = this.cache.get(key);
if (!cached) return null;
const { data, timestamp } = cached;
const isExpired = Date.now() - timestamp > this.TTL;
if (isExpired) {
this.cache.delete(key);
return null;
}
return data;
}
private setCache(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
基于装饰器的缓存控制:
为了创建更通用的解决方案,我们可以结合自定义装饰器和 Reflector 来实现更灵活的缓存控制:
自定义缓存装饰器:
import { SetMetadata } from '@nestjs/common';
export const CACHE_KEY = 'cache';
export const CACHE_TTL_KEY = 'cache_ttl';
/**
* 启用缓存装饰器
* @param ttl 缓存时间(毫秒),默认60秒
*/
export const Cacheable = (ttl: number = 60000) => SetMetadata(CACHE_KEY, ttl);
/**
* 设置缓存TTL装饰器
* @param ttl 缓存时间(毫秒)
*/
export const CacheTTL = (ttl: number) => SetMetadata(CACHE_TTL_KEY, ttl);
增强的缓存拦截器:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CACHE_KEY, CACHE_TTL_KEY } from './cache.decorator';
@Injectable()
export class ReflectorCacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const handler = context.getHandler();
const controller = context.getClass();
// 检查是否启用了缓存
const cacheTTL = this.reflector.getAllAndOverride<number>(CACHE_KEY, [
handler,
controller,
]);
if (!cacheTTL) {
// 未启用缓存,直接执行正常流程
return next.handle();
}
const request = context.switchToHttp().getRequest();
const cacheKey = this.generateCacheKey(request);
// 检查缓存
const cachedData = this.getFromCache(cacheKey, cacheTTL);
if (cachedData) {
return of(cachedData);
}
// 执行正常流程并缓存结果
return next.handle().pipe(
tap(response => {
this.setCache(cacheKey, response);
})
);
}
private generateCacheKey(request: any): string {
const { method, url, query, body } = request;
// 对于GET请求,只考虑URL和查询参数
if (method === 'GET') {
return `${method}:${url}:${JSON.stringify(query)}`;
}
// 对于其他请求,还要考虑请求体
return `${method}:${url}:${JSON.stringify(query)}:${JSON.stringify(body)}`;
}
private getFromCache(key: string, ttl: number): any {
const cached = this.cache.get(key);
if (!cached) return null;
const { data, timestamp } = cached;
const isExpired = Date.now() - timestamp > ttl;
if (isExpired) {
this.cache.delete(key);
return null;
}
return data;
}
private setCache(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
使用示例:
import { Controller, Get, Query } from '@nestjs/common';
import { Cacheable } from './cache.decorator';
@Controller('users')
export class UsersController {
@Get()
@Cacheable(30000) // 缓存30秒
async findAll(@Query() query: any) {
// 这个方法的结果会被缓存30秒
console.log('执行数据库查询...');
return await this.userService.findAll(query);
}
@Get('profile')
@Cacheable(120000) // 缓存2分钟
async getProfile(@Query('id') id: string) {
console.log('获取用户资料...');
return await this.userService.findById(id);
}
@Get('no-cache')
async getNoCacheData() {
// 这个方法不会被缓存
return await this.userService.getRealTimeData();
}
}
条件性响应拦截器:
除了缓存,流覆盖还可以用于实现条件性响应,比如根据用户权限返回不同的数据:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class ConditionalResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const user = request.user;
// 如果用户未登录,返回公开数据
if (!user) {
return of({
message: '请登录后查看完整内容',
publicData: '这是公开可见的数据'
});
}
// 如果用户权限不足,返回受限数据
if (user.role !== 'admin') {
return of({
message: '权限受限,显示部分内容',
limitedData: '这是受限用户可见的数据'
});
}
// 管理员用户,执行正常流程
return next.handle();
}
}
请求限流拦截器:
流覆盖还可以用于实现请求限流:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of, throwError } from 'rxjs';
import { TooManyRequestsException } from '@nestjs/common';
@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
private requests = new Map<string, number[]>();
private readonly limit = 10; // 每分钟最多10次请求
private readonly windowMs = 60000; // 时间窗口:1分钟
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const clientId = this.getClientId(request);
if (this.isRateLimited(clientId)) {
// 超出限制,直接返回错误响应
return throwError(() => new TooManyRequestsException('请求过于频繁,请稍后再试'));
}
// 记录请求并继续执行
this.recordRequest(clientId);
return next.handle();
}
private getClientId(request: any): string {
// 可以基于IP、用户ID或其他标识符
return request.ip || request.user?.id || 'anonymous';
}
private isRateLimited(clientId: string): boolean {
const now = Date.now();
const requests = this.requests.get(clientId) || [];
// 清理过期的请求记录
const validRequests = requests.filter(time => now - time < this.windowMs);
this.requests.set(clientId, validRequests);
return validRequests.length >= this.limit;
}
private recordRequest(clientId: string): void {
const requests = this.requests.get(clientId) || [];
requests.push(Date.now());
this.requests.set(clientId, requests);
}
}
最佳实践:
合理使用:避免过度使用流覆盖导致逻辑复杂化
缓存策略:选择合适的缓存键、TTL时间,注意缓存清理
性能优化:快速检查缓存,避免耗时操作,考虑使用Redis
异常处理:确保正确处理流覆盖时的异常情况
充分测试:编写单元测试确保各分支正确工作
通过流覆盖技术,可以为应用添加缓存、限流等功能,提升性能和用户体验。
更多运算符
RxJS 运算符为我们提供了强大的流操作能力,使得拦截器能够处理各种复杂的业务场景。通过合理组合不同的运算符,我们可以实现超时控制、重试机制、防抖节流、资源清理等高级功能。
超时处理:
处理路由请求超时是一个常见需求。当端点在指定时间内未返回响应时,我们希望主动终止请求并返回超时错误。这不仅能提升用户体验,还能避免资源浪费。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000), // 5秒超时
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
}
}
上述代码在5秒后会取消请求处理。你还可以在抛出 RequestTimeoutException
之前添加自定义逻辑,比如释放资源、记录日志等。
增强版超时拦截器:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException, Logger } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout, finalize } from 'rxjs/operators';
@Injectable()
export class EnhancedTimeoutInterceptor implements NestInterceptor {
private readonly logger = new Logger(EnhancedTimeoutInterceptor.name);
private readonly defaultTimeout = 5000; // 默认5秒
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
// 可以根据不同的路由设置不同的超时时间
const timeoutMs = this.getTimeoutForRoute(method, url);
const startTime = Date.now();
return next.handle().pipe(
timeout(timeoutMs),
catchError(err => {
if (err instanceof TimeoutError) {
const duration = Date.now() - startTime;
this.logger.warn(`请求超时: ${method} ${url} - 耗时: ${duration}ms, 超时设置: ${timeoutMs}ms`);
// 可以在这里添加清理逻辑
this.cleanupResources(context);
return throwError(() => new RequestTimeoutException(`请求超时,请稍后重试`));
}
return throwError(() => err);
}),
finalize(() => {
const duration = Date.now() - startTime;
this.logger.debug(`请求完成: ${method} ${url} - 耗时: ${duration}ms`);
})
);
}
private getTimeoutForRoute(method: string, url: string): number {
// 根据路由特征设置不同的超时时间
if (url.includes('/upload') || url.includes('/download')) {
return 30000; // 文件操作30秒
}
if (url.includes('/report') || url.includes('/export')) {
return 60000; // 报表生成60秒
}
return this.defaultTimeout; // 默认5秒
}
private cleanupResources(context: ExecutionContext): void {
// 在这里可以添加资源清理逻辑
// 比如取消数据库查询、关闭文件句柄等
this.logger.debug('执行超时清理逻辑');
}
}
重试机制:
对于可能因为网络波动或临时服务不可用而失败的请求,我们可以实现自动重试机制:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable, throwError, timer } from 'rxjs';
import { catchError, retry, retryWhen, delayWhen, take, concat } from 'rxjs/operators';
@Injectable()
export class RetryInterceptor implements NestInterceptor {
private readonly logger = new Logger(RetryInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
return next.handle().pipe(
// 简单重试:失败后立即重试2次
// retry(2),
// 智能重试:带延迟的重试机制
retryWhen(errors =>
errors.pipe(
delayWhen((error, index) => {
const retryAttempt = index + 1;
const delay = Math.min(1000 * Math.pow(2, index), 10000); // 指数退避,最大10秒
// 只对特定错误进行重试
if (this.shouldRetry(error) && retryAttempt <= 3) {
this.logger.warn(`请求失败,${delay}ms后进行第${retryAttempt}次重试: ${method} ${url}`);
return timer(delay);
}
// 不重试,直接抛出错误
return throwError(() => error);
}),
take(3) // 最多重试3次
)
),
catchError(error => {
this.logger.error(`请求最终失败: ${method} ${url}`, error.stack);
return throwError(() => error);
})
);
}
private shouldRetry(error: any): boolean {
// 定义哪些错误需要重试
const retryableErrors = [
'ECONNRESET',
'ETIMEDOUT',
'ECONNREFUSED',
'ENOTFOUND'
];
// HTTP状态码重试策略
const retryableStatusCodes = [502, 503, 504];
return (
retryableErrors.includes(error.code) ||
retryableStatusCodes.includes(error.status) ||
error.message?.includes('timeout')
);
}
}
防抖和节流:
对于频繁触发的请求,我们可以使用防抖(debounce)和节流(throttle)来优化性能:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { debounceTime, throttleTime, distinctUntilChanged } from 'rxjs/operators';
@Injectable()
export class DebounceInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 对搜索接口应用防抖
if (request.url.includes('/search')) {
return next.handle().pipe(
debounceTime(300), // 300ms内的重复请求会被忽略
distinctUntilChanged() // 过滤相同的请求
);
}
// 对统计接口应用节流
if (request.url.includes('/stats')) {
return next.handle().pipe(
throttleTime(1000) // 1秒内最多执行一次
);
}
return next.handle();
}
}
请求合并和切换:
使用 switchMap
可以实现请求切换,确保只处理最新的请求:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { switchMap, mergeMap, concatMap } from 'rxjs/operators';
@Injectable()
export class RequestSwitchInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 对于搜索请求,使用switchMap确保只处理最新的搜索
if (request.url.includes('/search')) {
return next.handle().pipe(
switchMap(result => {
// 如果有新的搜索请求进来,会取消当前请求
return result;
})
);
}
// 对于并发请求,使用mergeMap
if (request.url.includes('/batch')) {
return next.handle().pipe(
mergeMap(result => {
// 允许多个请求并发执行
return result;
})
);
}
// 对于需要顺序执行的请求,使用concatMap
if (request.url.includes('/sequence')) {
return next.handle().pipe(
concatMap(result => {
// 确保请求按顺序执行
return result;
})
);
}
return next.handle();
}
}
资源清理:
使用 finalize
运算符可以确保在请求完成或出错时都能执行清理逻辑:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
@Injectable()
export class ResourceCleanupInterceptor implements NestInterceptor {
private readonly logger = new Logger(ResourceCleanupInterceptor.name);
private activeConnections = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const requestId = this.generateRequestId(request);
// 记录活跃连接
this.activeConnections.set(requestId, {
startTime: Date.now(),
method: request.method,
url: request.url
});
return next.handle().pipe(
tap(response => {
// 请求成功时的处理
this.logger.debug(`请求成功: ${requestId}`);
}),
finalize(() => {
// 无论成功还是失败都会执行的清理逻辑
const connection = this.activeConnections.get(requestId);
if (connection) {
const duration = Date.now() - connection.startTime;
this.logger.debug(`清理资源: ${requestId}, 耗时: ${duration}ms`);
// 执行具体的清理操作
this.cleanupConnection(requestId);
this.activeConnections.delete(requestId);
}
})
);
}
private generateRequestId(request: any): string {
return `${request.method}-${request.url}-${Date.now()}-${Math.random()}`;
}
private cleanupConnection(requestId: string): void {
// 在这里执行具体的清理逻辑
// 比如关闭数据库连接、清理临时文件、释放内存等
this.logger.debug(`执行连接清理: ${requestId}`);
}
// 获取当前活跃连接数
getActiveConnectionsCount(): number {
return this.activeConnections.size;
}
}
延迟响应:
有时我们需要人为地延迟响应,比如模拟慢网络或实现渐进式加载:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { delay, delayWhen, timer } from 'rxjs/operators';
@Injectable()
export class DelayInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 根据请求类型设置不同的延迟
const delayMs = this.getDelayForRequest(request);
if (delayMs > 0) {
return next.handle().pipe(
delay(delayMs) // 固定延迟
);
}
return next.handle();
}
private getDelayForRequest(request: any): number {
// 开发环境模拟慢网络
if (process.env.NODE_ENV === 'development') {
if (request.url.includes('/slow-api')) {
return 2000; // 2秒延迟
}
if (request.url.includes('/simulate-loading')) {
return Math.random() * 1000; // 随机延迟0-1秒
}
}
return 0; // 生产环境不延迟
}
}
组合使用多个运算符:
在实际项目中,我们经常需要组合使用多个运算符来实现复杂的功能:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable, throwError, timer } from 'rxjs';
import {
catchError,
timeout,
retry,
finalize,
tap,
map,
retryWhen,
delayWhen,
take
} from 'rxjs/operators';
@Injectable()
export class ComprehensiveInterceptor implements NestInterceptor {
private readonly logger = new Logger(ComprehensiveInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const startTime = Date.now();
this.logger.log(`开始处理请求: ${method} ${url}`);
return next.handle().pipe(
// 1. 设置超时
timeout(10000),
// 2. 重试机制
retryWhen(errors =>
errors.pipe(
delayWhen((error, index) => {
if (index < 2 && this.shouldRetry(error)) {
const delay = 1000 * (index + 1);
this.logger.warn(`重试请求: ${method} ${url}, 延迟: ${delay}ms`);
return timer(delay);
}
return throwError(() => error);
}),
take(2)
)
),
// 3. 响应数据转换
map(response => {
// 可以在这里对响应数据进行统一处理
return {
success: true,
data: response,
timestamp: new Date().toISOString()
};
}),
// 4. 成功时的副作用操作
tap(response => {
const duration = Date.now() - startTime;
this.logger.log(`请求成功: ${method} ${url}, 耗时: ${duration}ms`);
}),
// 5. 错误处理
catchError(error => {
const duration = Date.now() - startTime;
this.logger.error(`请求失败: ${method} ${url}, 耗时: ${duration}ms`, error.stack);
return throwError(() => error);
}),
// 6. 最终清理
finalize(() => {
const duration = Date.now() - startTime;
this.logger.debug(`请求处理完成: ${method} ${url}, 总耗时: ${duration}ms`);
})
);
}
private shouldRetry(error: any): boolean {
const retryableErrors = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'];
const retryableStatusCodes = [502, 503, 504];
return (
retryableErrors.includes(error.code) ||
retryableStatusCodes.includes(error.status)
);
}
}
最佳实践:
选择合适的运算符:根据具体需求选择最适合的RxJS运算符,避免过度复杂化
性能考虑:
- 避免在拦截器中使用过多的运算符链
- 注意内存泄漏,确保Observable正确完成
- 合理设置超时和重试次数
错误处理:
- 始终包含适当的错误处理逻辑
- 区分可重试和不可重试的错误
- 提供有意义的错误信息
日志记录:
- 记录关键操作的日志
- 包含足够的上下文信息
- 使用适当的日志级别
测试覆盖:
- 为复杂的运算符组合编写单元测试
- 测试各种异常情况
- 验证资源清理逻辑
通过合理使用RxJS运算符,拦截器可以成为处理横切关注点的强大工具,帮助我们构建更加健壮和高性能的应用程序。