{T}

NestJS全局异常捕获进阶学习笔记

NestJS全局异常捕获进阶学习笔记

核心知识点

1. 全局 Exception Filter 只能有一个

1.1 重要限制

typescript
//  错误写法:注册了两个全局 Filter,只有最后一个会生效
app.useGlobalFilters(new HttpExceptionFilter(), new AllExceptionsFilter());
// → 只有 AllExceptionsFilter 会执行,HttpExceptionFilter 被覆盖!

1.2 解决思路

将所有异常捕获逻辑合并到一个 Filter 类中,通过条件判断处理不同类型的异常:

typescript
@Catch()  // 不传参数 → 捕获所有异常
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const isHttpException = exception instanceof HttpException;

    if (isHttpException) {
      // HTTP 异常的处理逻辑
    } else {
      // 非 HTTP 异常的处理逻辑
    }
  }
}

2. @Catch 不传参数 = 捕获所有异常

typescript
// @Catch(HttpException)   → 只捕获 HTTP 异常
// @Catch()                → 捕获所有异常(HTTP + WebSocket + 其他)
写法捕获范围
@Catch(HttpException)仅 HTTP 异常(404、403、500 等)
@Catch(WsException)仅 WebSocket 异常
@Catch()所有异常(HTTP + WebSocket + 运行时异常等)

使用 @Catch() 时,exception 参数类型变为 unknown,需要在 catch() 方法中自行判断异常类型。


3. 获取客户端真实 IP(request-ip)

3.1 安装

bash
pnpm install request-ip

3.2 使用方法

typescript
import * as requestIP from 'request-ip';

// 在过滤器中获取客户端 IP
const clientIp = requestIP.getClientIp(request);

3.3 为什么需要获取客户端 IP?

场景用途
错误日志记录哪个 IP 触发了异常,便于追溯
安全审计记录敏感操作来源
频率限制同一 IP 频繁异常可触发告警
用户排查根据日志中的 IP 联系用户确认问题

4. 请求信息采集

4.1 可采集的请求信息

typescript
const request = ctx.getRequest<Request>();

const requestInfo = {
  ip: requestIP.getClientIp(request),       // 客户端 IP
  method: request.method,                    // 请求方法:GET / POST
  url: request.url,                          // 请求路径:/api/v1/user/123
  body: request.body,                        // 请求体参数
  params: request.params,                    // 路径参数:/user/:id → { id: '123' }
  query: request.query,                      // 查询参数:?page=1 → { page: '1' }
  headers: request.headers,                  // 请求头信息
  timestamp: new Date().toISOString(),       // 请求时间
};

4.2 undefined 字段自动过滤

如果某个字段值为 undefined,在 response.json() 响应给前端时,该字段会被自动忽略,不会出现在响应 JSON 中。

typescript
const errorResponse = {
  code: 500,
  message: 'Internal Server Error',
  headers: undefined,    // ← 这个字段不会出现在响应中
};

response.status(500).json(errorResponse);

// 实际响应给前端:
// { "code": 500, "message": "Internal Server Error" }
// headers 字段被自动过滤掉了

代码实战案例

需求描述

实现一个全局异常捕获过滤器,捕获所有类型的异常,采集完整的请求信息(IP、method、body、params、query、headers),返回标准化错误响应,并记录到日志文件中。

完整实现

第一步:安装依赖

bash
pnpm install request-ip @types/request-ip -D

第二步:创建全局异常过滤器

typescript
// src/filters/all-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import * as requestIP from 'request-ip';

@Catch()  // 不传参数:捕获所有异常
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest<Request>();
    const response = ctx.getResponse<Response>();

    // 获取客户端 IP
    const clientIp = requestIP.getClientIp(request);

    // 判断异常类型
    const isHttpException = exception instanceof HttpException;

    const status = isHttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = isHttpException
      ? exception.message
      : 'Internal Server Error';

    // 采集完整请求信息
    const requestInfo = {
      ip: clientIp,
      method: request.method,
      url: request.url,
      body: request.body,
      params: request.params,
      query: request.query,
      headers: request.headers,
      timestamp: new Date().toISOString(),
    };

    // 构造错误响应(undefined 字段不会出现在 JSON 中)
    const errorResponse = {
      code: status,
      message,
      ...requestInfo,
    };

    // 记录错误日志
    this.logger.error(
      `${request.method} ${request.url} - ${message}`,
      exception instanceof Error ? exception.stack : String(exception),
    );

    // 返回响应
    response.status(status).json(errorResponse);
  }
}

第三步:注册全局过滤器

typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './filters/all-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 注册全局过滤器(只能有一个)
  app.useGlobalFilters(new AllExceptionsFilter());

  await app.listen(3000);
}
bootstrap();

第四步:测试

bash
# 测试 1:请求不存在的路由
curl http://localhost:3000/api/v1/not-exist

# 测试 2:带参数的请求
curl "http://localhost:3000/user/123?page=1" -X POST -H "Content-Type: application/json" -d '{"name":"test"}'

响应示例

json
{
  "code": 404,
  "message": "Cannot GET /api/v1/not-exist",
  "ip": "::1",
  "method": "GET",
  "url": "/api/v1/not-exist",
  "body": {},
  "params": {},
  "query": {},
  "headers": {
    "user-agent": "curl/8.0",
    "host": "localhost:3000",
    "accept": "*/*"
  },
  "timestamp": "2026-03-30T09:00:00.000Z"
}

日志文件记录(logs/application-2026-03-30.log):

code
2026-03-30T09:00:00.000Z error: GET /api/v1/not-exist - Cannot GET /api/v1/not-exist
    Error: Cannot GET /api/v1/not-exist
        at ...(完整堆栈信息)

常见问题与解决方案

问题原因解决方案
多个全局 Filter 只有一个生效全局 Filter 只能有一个合并所有逻辑到一个 Filter 类中
exception 类型为 unknown@Catch() 未指定类型使用 instanceof 判断异常类型
客户端 IP 始终为 ::ffff:127.0.0.1本地开发环境代理生产环境部署后为真实 IP
响应中有多余的 undefined 字段不会出现JSON.stringify() 自动忽略 undefined
非 HTTP 异常没有 getStatus()只有 HttpException 有此方法判断类型后使用默认 500 状态码

学习要点总结

  1. 全局 Exception Filter 只能有一个:多个全局 Filter 注册时,后面的会覆盖前面的,需合并逻辑
  2. @Catch() 不传参数:捕获所有异常,exception 类型变为 unknown,需用 instanceof 判断
  3. request-ip 获取客户端 IPrequestIP.getClientIp(request) 一行搞定
  4. undefined 字段自动过滤response.json() 会自动忽略值为 undefined 的字段
  5. 请求信息全面采集:IP、method、url、body、params、query、headers → 完整的审计日志

延伸学习资源

官方文档

后续课程预告

  • 守卫(Guards):认证与授权机制
  • 拦截器(Interceptors):请求/响应转换、缓存、日志

本节作业回顾

作业解答要点
全局捕获所有异常@Catch() 不传参数
获取客户端 IP安装 request-ip,使用 getClientIp()
采集请求信息request.body / params / query / headers
过滤 undefined 字段response.json() 自动处理