异常过滤器(Filter):如何统一处理抛出的 HttpException?
在 NestJS 应用中,异常处理是确保应用稳定性和提供良好用户体验的重要组成部分。异常过滤器(Exception Filter)是 NestJS 提供的一种机制,用于捕获和处理应用中抛出的异常,并将它们转换为适当的 HTTP 响应。本文将深入探讨异常过滤器的工作原理,以及如何使用它们统一处理 HttpException 和其他类型的异常。
1. 异常过滤器基础概念
1.1 什么是异常过滤器?
异常过滤器是实现了 ExceptionFilter 接口的类,用于捕获和处理异常:
typescript
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class AnyExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
response
.status(500)
.json({
statusCode: 500,
timestamp: new Date().toISOString(),
path: request.url,
message: 'Internal server error',
});
}
}1.2 异常处理流程
异常过滤器在请求处理流程中的位置:
Client → HTTP Request → Router → Guards → Interceptors (pre) → Pipes → Controller Handler
↓
Exception → Exception Filters → Response
↑
Interceptors (post)当控制器处理函数或其他组件抛出异常时,异常过滤器会捕获这些异常并生成适当的响应。
2. 内置异常类型
2.1 HTTP 异常
NestJS 提供了多种内置的 HTTP 异常:
typescript
import {
BadRequestException,
UnauthorizedException,
NotFoundException,
ForbiddenException,
ConflictException,
InternalServerErrorException
} from '@nestjs/common';
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('id') id: string) {
if (!id) {
throw new BadRequestException('ID is required');
}
const user = this.userService.findById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
try {
return this.userService.create(createUserDto);
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
throw new ConflictException('User already exists');
}
throw new InternalServerErrorException('Failed to create user');
}
}
}2.2 自定义 HTTP 异常
typescript
// 创建自定义 HTTP 异常
export class UserNotActiveException extends HttpException {
constructor(userId: string) {
super(
{
statusCode: 403,
message: `User ${userId} is not active`,
error: 'User Not Active',
},
403,
);
}
}
// 使用自定义异常
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.userService.findById(id);
if (user && !user.isActive) {
throw new UserNotActiveException(id);
}
return user;
}
}3. 异常过滤器实现
3.1 捕获特定异常
typescript
// 捕获特定类型的异常
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
...(typeof exceptionResponse === 'object' ? exceptionResponse : {}),
});
}
}3.2 捕获所有异常
typescript
// 捕获所有类型的异常
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
// 处理不同类型的异常
if (exception instanceof HttpException) {
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
...(typeof exceptionResponse === 'object' ? exceptionResponse : { message: exceptionResponse }),
});
} else {
// 处理非 HTTP 异常
console.error('Unexpected error:', exception);
response
.status(500)
.json({
statusCode: 500,
timestamp: new Date().toISOString(),
path: request.url,
message: 'Internal server error',
});
}
}
}4. 异常过滤器应用级别
4.1 全局异常过滤器
typescript
// 全局应用异常过滤器
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 应用全局异常过滤器
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}4.2 控制器级别异常过滤器
typescript
// 控制器级别应用异常过滤器
@UseFilters(new HttpExceptionFilter())
@Controller('users')
export class UserController {
// 控制器方法
}4.3 路由级别异常过滤器
typescript
// 路由级别应用异常过滤器
@UseFilters(new CustomExceptionFilter())
@Get(':id')
findOne(@Param('id') id: string) {
// 路由逻辑
}5. 自定义异常过滤器
5.1 业务异常过滤器
typescript
// 业务相关的异常过滤器
@Catch(UserNotActiveException, UserNotFoundException)
export class UserExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
errorCode: this.getErrorCode(exception),
suggestion: this.getSuggestion(exception),
});
}
private getErrorCode(exception: HttpException): string {
if (exception instanceof UserNotActiveException) {
return 'USER_NOT_ACTIVE';
}
if (exception instanceof UserNotFoundException) {
return 'USER_NOT_FOUND';
}
return 'UNKNOWN_ERROR';
}
private getSuggestion(exception: HttpException): string {
if (exception instanceof UserNotActiveException) {
return 'Please contact administrator to activate your account';
}
if (exception instanceof UserNotFoundException) {
return 'Please check the user ID and try again';
}
return 'Please try again later';
}
}5.2 多语言异常过滤器
typescript
// 支持多语言的异常过滤器
@Catch()
export class I18nExceptionFilter implements ExceptionFilter {
constructor(private readonly i18n: I18nService) {}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const acceptLanguage = request.headers['accept-language'] || 'en';
let message = 'Internal server error';
let status = 500;
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'object' && 'message' in exceptionResponse) {
message = exceptionResponse['message'];
} else {
message = exception.message;
}
}
// 翻译消息
const translatedMessage = this.i18n.translate(message, {
lang: acceptLanguage,
});
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: translatedMessage,
});
}
}6. 异常过滤器与日志集成
6.1 错误日志记录
typescript
// 集成日志记录的异常过滤器
@Catch()
export class LoggingExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: Logger) {}
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;
// 记录错误日志
this.logger.error(
`HTTP ${status} - ${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : String(exception),
'ExceptionFilter',
);
// 构建响应
const responseBody = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: this.getErrorMessage(exception),
};
response.status(status).json(responseBody);
}
private getErrorMessage(exception: unknown): string {
if (exception instanceof HttpException) {
const response = exception.getResponse();
return typeof response === 'string'
? response
: (response as any).message || exception.message;
}
if (exception instanceof Error) {
return exception.message;
}
return 'Internal server error';
}
}6.2 结构化错误日志
typescript
// 结构化错误日志记录
@Catch()
export class StructuredLoggingExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: Logger) {}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const errorInfo = this.buildErrorInfo(exception, request);
// 记录结构化日志
this.logger.error(
'Request failed',
JSON.stringify(errorInfo, null, 2),
'ExceptionFilter',
);
response.status(errorInfo.status).json(errorInfo.response);
}
private buildErrorInfo(exception: unknown, request: any) {
const timestamp = new Date().toISOString();
const status = exception instanceof HttpException ? exception.getStatus() : 500;
return {
timestamp,
status,
method: request.method,
url: request.url,
userAgent: request.headers['user-agent'],
ip: request.ip,
exception: {
name: exception?.constructor?.name,
message: exception instanceof Error ? exception.message : String(exception),
stack: exception instanceof Error ? exception.stack : undefined,
},
response: {
statusCode: status,
timestamp,
path: request.url,
message: this.getErrorMessage(exception),
},
};
}
private getErrorMessage(exception: unknown): string {
if (exception instanceof HttpException) {
const response = exception.getResponse();
return typeof response === 'string'
? response
: (response as any).message || exception.message;
}
return 'Internal server error';
}
}7. 异常过滤器最佳实践
7.1 分层异常处理
typescript
// 分层异常处理策略
// 1. 全局异常过滤器 - 处理未捕获的异常
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// 记录严重错误并返回通用错误响应
}
}
// 2. HTTP 异常过滤器 - 处理标准 HTTP 异常
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// 处理标准 HTTP 响应
}
}
// 3. 业务异常过滤器 - 处理特定业务异常
@Catch(BusinessException)
export class BusinessExceptionFilter implements ExceptionFilter {
catch(exception: BusinessException, host: ArgumentsHost) {
// 处理业务相关异常
}
}7.2 异常响应标准化
typescript
// 标准化异常响应格式
export interface ErrorResponse {
statusCode: number;
message: string;
error?: string;
timestamp: string;
path: string;
details?: any;
}
@Catch()
export class StandardExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const timestamp = new Date().toISOString();
const errorResponse: ErrorResponse = {
statusCode: 500,
message: 'Internal server error',
timestamp,
path: request.url,
};
if (exception instanceof HttpException) {
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
errorResponse.statusCode = status;
if (typeof exceptionResponse === 'object') {
Object.assign(errorResponse, exceptionResponse);
} else {
errorResponse.message = exceptionResponse as string;
}
}
response.status(errorResponse.statusCode).json(errorResponse);
}
}8. 总结
异常过滤器是 NestJS 中处理异常的重要机制:
- 捕获时机:在请求处理流程的任何阶段抛出的异常都可以被捕获
- 应用级别:支持全局、控制器和路由级别的应用
- 异常类型:可以捕获特定类型或所有类型的异常
- 响应处理:将异常转换为标准化的 HTTP 响应
- 扩展能力:支持自定义异常处理逻辑
通过合理使用异常过滤器,我们可以:
- 提供一致的错误响应格式
- 增强应用的健壮性和用户体验
- 集成日志记录和监控
- 实现多语言和国际化支持
- 处理业务特定的异常场景
在下一篇文章中,我们将探讨守卫(Guard)与拦截器(Interceptor)的执行顺序,了解请求进来时:Guard → Interceptor → Pipe → Controller → Interceptor → 返回的完整流程。