6月5日 20:04

NestJS 拦截器和异常过滤器:响应转换、日志、缓存和统一错误处理

拦截器和异常过滤器是 NestJS 请求处理管道中的两个关键环节。拦截器在请求成功时介入,做响应转换、日志记录、缓存等;异常过滤器在请求出错时介入,统一错误格式。两者配合,就能控制 API 返回给客户端的所有内容。

先搞清执行顺序

NestJS 处理一个请求的完整流程:

shell
请求 → Middleware → Guard → Interceptor() → Controller → Interceptor() → 响应 (异常) Exception Filter → 错误响应

拦截器包住了 Controller——请求进来时先执行拦截器的"前"逻辑,Controller 处理完后执行"后"逻辑。如果 Controller 抛异常,跳过"后"逻辑,直接进异常过滤器。

拦截器:请求前后的横切逻辑

响应格式统一

后端 API 最常见的需求:所有响应都包成统一格式 { code, data, message }

typescript
// interceptors/transform.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { map } from 'rxjs/operators'; export interface ApiResponse<T> { code: number; data: T; message: string; } @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> { return next.handle().pipe( map(data => ({ code: 0, data, message: 'success', })), ); } }

next.handle() 返回的是 Controller 的原始响应 Observable,用 map 操作符在"后"阶段转换格式。

请求耗时日志

typescript
@Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('HTTP'); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const { method, url } = request; const now = Date.now(); this.logger.log(`${method} ${url} - Started`); return next.handle().pipe( tap(() => { this.logger.log(`${method} ${url} - ${Date.now() - now}ms`); }), ); } }

tap 操作符只观察不修改——适合做日志、指标采集等副作用操作。

缓存拦截器

typescript
@Injectable() export class CacheInterceptor implements NestInterceptor { private cache = new Map<string, { data: any; expire: number }>(); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const key = `${request.method}:${request.url}`; const cached = this.cache.get(key); if (cached && cached.expire > Date.now()) { return of(cached.data); // 命中缓存,直接返回,不进 Controller } return next.handle().pipe( tap(data => { this.cache.set(key, { data, expire: Date.now() + 60000 }); // 缓存 1 分钟 }), ); } }

这个拦截器的特别之处:of(cached.data) 直接返回数据,next.handle() 根本不会执行——Controller 被完全跳过了。这就是拦截器的强大之处:它可以选择不调用 Controller。

超时控制

typescript
import { timeout, TimeoutError } from 'rxjs/operators'; import { throwError } from 'rxjs'; @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); }), ); } }

绑定拦截器

typescript
// 方法级别 @UseInterceptors(LoggingInterceptor) @Get('users') getUsers() {} // Controller 级别 @UseInterceptors(TransformInterceptor) @Controller('users') export class UserController {} // 全局 app.useGlobalInterceptors(new TransformInterceptor());

全局绑定时用 useGlobalInterceptors 简单直接,但如果拦截器需要依赖注入,必须用 Module 方式注册:

typescript
// 在任意 Module 中 providers: [ { provide: APP_INTERCEPTOR, useClass: TransformInterceptor, }, ]

异常过滤器:统一错误处理

基本异常过滤器

NestJS 内置了默认的异常处理,但返回格式不够友好。自定义过滤器可以统一错误格式:

typescript
// filters/http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; import { Response } from 'express'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const status = exception.getStatus(); const exceptionResponse = exception.getResponse(); response.status(status).json({ code: status, message: typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message || exception.message, data: null, timestamp: new Date().toISOString(), }); } }

捕获所有异常

@Catch() 不带参数捕获一切异常——包括非 HttpException 的错误(如数据库异常、第三方库异常):

typescript
@Catch() export class AllExceptionsFilter implements ExceptionFilter { private readonly logger = new Logger('ExceptionFilter'); catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception instanceof HttpException ? exception.getStatus() : 500; // 非业务异常记录完整堆栈 if (!(exception instanceof HttpException)) { this.logger.error(exception instanceof Error ? exception.stack : String(exception)); } response.status(status).json({ code: status, message: status === 500 ? '服务器内部错误' : (exception as any).message, data: null, path: request.url, timestamp: new Date().toISOString(), }); } }

500 错误不要把内部细节暴露给客户端——统一返回"服务器内部错误",详细信息只写日志。

业务异常的优雅处理

定义业务异常码,让过滤器按类型处理:

typescript
// 自定义业务异常 export class BusinessException extends HttpException { private readonly errorCode: string; constructor(errorCode: string, message: string, statusCode = 400) { super(message, statusCode); this.errorCode = errorCode; } getErrorCode() { return this.errorCode; } } // 在 Service 中使用 async transfer(fromId: number, toId: number, amount: number) { if (amount <= 0) { throw new BusinessException('INVALID_AMOUNT', '转账金额必须大于0'); } const from = await this.findOne(fromId); if (from.balance < amount) { throw new BusinessException('INSUFFICIENT_BALANCE', '余额不足'); } // ... 执行转账 } // 过滤器中区分处理 @Catch(BusinessException) export class BusinessExceptionFilter implements ExceptionFilter { catch(exception: BusinessException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); response.status(exception.getStatus()).json({ code: exception.getErrorCode(), // 'INSUFFICIENT_BALANCE' message: exception.message, // '余额不足' data: null, }); } }

绑定过滤器

typescript
// 方法级别 @UseFilters(HttpExceptionFilter) @Get('users') getUsers() {} // 全局 app.useGlobalFilters(new AllExceptionsFilter()); // 全局(支持依赖注入) providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter, }, ]

拦截器 vs 异常过滤器的边界

场景用哪个原因
响应格式统一拦截器处理正常响应
日志记录拦截器记录请求和响应
缓存拦截器控制是否调用 Controller
超时控制拦截器用 RxJS timeout 操作符
错误格式统一异常过滤器处理异常响应
错误日志异常过滤器记录异常堆栈
错误码映射异常过滤器把技术异常翻译成业务错误码

一个常见错误:在拦截器里用 catchError 处理异常。虽然技术上可行,但违反了职责分离——拦截器负责"正常路径",过滤器负责"异常路径"。混在一起会让代码难以维护。

全局注册的最佳实践

main.ts 中统一注册全局拦截器和过滤器:

typescript
// main.ts const app = await NestFactory.create(AppModule); app.useGlobalInterceptors( new TransformInterceptor(), new LoggingInterceptor(), ); app.useGlobalFilters( new AllExceptionsFilter(), new BusinessExceptionFilter(), ); await app.listen(3000);

需要注意注册顺序:过滤器按注册顺序反向执行(后注册的先执行),拦截器按注册顺序正向执行。所以 AllExceptionsFilter 放最后——它兜底处理所有未捕获的异常。

标签:NestJS