Skip to content

异常过滤器(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 中处理异常的重要机制:

  1. 捕获时机:在请求处理流程的任何阶段抛出的异常都可以被捕获
  2. 应用级别:支持全局、控制器和路由级别的应用
  3. 异常类型:可以捕获特定类型或所有类型的异常
  4. 响应处理:将异常转换为标准化的 HTTP 响应
  5. 扩展能力:支持自定义异常处理逻辑

通过合理使用异常过滤器,我们可以:

  1. 提供一致的错误响应格式
  2. 增强应用的健壮性和用户体验
  3. 集成日志记录和监控
  4. 实现多语言和国际化支持
  5. 处理业务特定的异常场景

在下一篇文章中,我们将探讨守卫(Guard)与拦截器(Interceptor)的执行顺序,了解请求进来时:Guard → Interceptor → Pipe → Controller → Interceptor → 返回的完整流程。