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-ip3.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 状态码 |
学习要点总结
- 全局 Exception Filter 只能有一个:多个全局 Filter 注册时,后面的会覆盖前面的,需合并逻辑
@Catch()不传参数:捕获所有异常,exception类型变为unknown,需用instanceof判断request-ip获取客户端 IP:requestIP.getClientIp(request)一行搞定undefined字段自动过滤:response.json()会自动忽略值为undefined的字段- 请求信息全面采集:IP、method、url、body、params、query、headers → 完整的审计日志
延伸学习资源
官方文档
后续课程预告
- 守卫(Guards):认证与授权机制
- 拦截器(Interceptors):请求/响应转换、缓存、日志
本节作业回顾
| 作业 | 解答要点 |
|---|---|
| 全局捕获所有异常 | @Catch() 不传参数 |
| 获取客户端 IP | 安装 request-ip,使用 getClientIp() |
| 采集请求信息 | request.body / params / query / headers |
| 过滤 undefined 字段 | response.json() 自动处理 |