NestJS异常过滤器 (Exception Filters)

学习目标

  • 理解异常过滤器在NestJS中的作用和地位
  • 掌握全局异常处理的实现方法
  • 学会创建和使用自定义异常
  • 理解HTTP异常的使用场景
  • 能够实现统一的错误响应格式

核心知识点

1. 异常过滤器概念

异常过滤器是NestJS中用于处理异常的组件,它们可以捕获应用程序中抛出的异常,并将其转换为适当的HTTP响应。异常过滤器的主要作用:

  • 捕获应用程序中的异常
  • 自定义错误响应格式
  • 记录异常信息
  • 处理不同类型的异常

2. 内置异常

NestJS提供了一系列内置的HTTP异常类,位于@nestjs/common包中:

  • HttpException:基础HTTP异常类
  • BadRequestException:400错误
  • UnauthorizedException:401错误
  • ForbiddenException:403错误
  • NotFoundException:404错误
  • InternalServerErrorException:500错误
  • 等等

使用内置异常:

import { Controller, Get, NotFoundException } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    const cat = this.catsService.findOne(id);
    if (!cat) {
      throw new NotFoundException(`Cat with id ${id} not found`);
    }
    return cat;
  }
}

3. 自定义异常

创建自定义异常类,继承自HttpException

// forbidden.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class ForbiddenException extends HttpException {
  constructor(message: string = 'Forbidden') {
    super(message, HttpStatus.FORBIDDEN);
  }
}

4. 异常过滤器创建

创建异常过滤器需要实现ExceptionFilter接口:

// http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, 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 request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: exception.getResponse(),
      });
  }
}

5. 应用异常过滤器

控制器级别

使用@UseFilters()装饰器在控制器级别应用异常过滤器:

import { Controller, Get, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';

@Controller('cats')
@UseFilters(new HttpExceptionFilter())
export class CatsController {
  // 控制器方法
}

方法级别

使用@UseFilters()装饰器在方法级别应用异常过滤器:

import { Controller, Get, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';

@Controller('cats')
export class CatsController {
  @Get()
  @UseFilters(new HttpExceptionFilter())
  findAll() {
    // 方法实现
  }
}

全局级别

在应用程序级别注册全局异常过滤器:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

6. 捕获所有异常

创建一个捕获所有异常的过滤器:

// all-exceptions.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    
    // 确定状态码
    const status = exception.getStatus ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
    
    // 确定错误消息
    const message = exception.message || 'Internal server error';

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message,
      });
  }
}

7. 依赖注入

异常过滤器可以通过依赖注入系统注入其他服务:

// logging-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, Inject } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Catch()
export class LoggingExceptionFilter implements ExceptionFilter {
  constructor(private loggerService: LoggerService) {}

  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
    
    // 记录异常
    this.loggerService.error(
      `Exception occurred: ${exception.message}`,
      exception.stack,
      `Request: ${request.method} ${request.url}`
    );
    
    // 继续处理异常
    // ...
  }
}

8. 异常过滤器链

可以同时应用多个异常过滤器,它们会按照注册顺序执行:

@Controller('cats')
@UseFilters(new LoggingExceptionFilter(), new HttpExceptionFilter())
export class CatsController {
  // 控制器方法
}

9. 自定义异常响应

可以根据不同的异常类型返回不同的响应格式:

// custom-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, BadRequestException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class CustomExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    // 根据异常类型返回不同的响应格式
    if (exception instanceof BadRequestException) {
      response
        .status(status)
        .json({
          statusCode: status,
          timestamp: new Date().toISOString(),
          path: request.url,
          errors: exception.getResponse(),
        });
    } else {
      response
        .status(status)
        .json({
          statusCode: status,
          timestamp: new Date().toISOString(),
          path: request.url,
          message: exception.getResponse(),
        });
    }
  }
}

实践案例分析

案例:统一错误处理系统

需求分析

我们需要创建一个统一的错误处理系统,包括:

  • 捕获所有类型的异常
  • 记录异常信息
  • 返回统一的错误响应格式
  • 区分不同环境的错误处理(开发环境显示详细错误,生产环境显示友好错误)

实现步骤

  1. 创建自定义异常类
  2. 创建全局异常过滤器
  3. 集成日志服务
  4. 测试错误处理系统

代码实现

1. 创建自定义异常类
// business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class BusinessException extends HttpException {
  constructor(message: string, statusCode: HttpStatus = HttpStatus.BAD_REQUEST) {
    super(message, statusCode);
  }
}
2. 创建日志服务
// logger.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class LoggerService {
  error(message: string, stack?: string, context?: string) {
    console.error(`[ERROR] ${context ? `[${context}] ` : ''}${message}`);
    if (stack) {
      console.error(stack);
    }
  }

  warn(message: string, context?: string) {
    console.warn(`[WARN] ${context ? `[${context}] ` : ''}${message}`);
  }

  info(message: string, context?: string) {
    console.info(`[INFO] ${context ? `[${context}] ` : ''}${message}`);
  }

  debug(message: string, context?: string) {
    console.debug(`[DEBUG] ${context ? `[${context}] ` : ''}${message}`);
  }
}
3. 创建全局异常过滤器
// global-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Inject } from '@nestjs/common';
import { Request, Response } from 'express';
import { LoggerService } from './logger.service';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(private loggerService: LoggerService) {}

  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    
    // 确定状态码
    const status = exception.getStatus ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
    
    // 确定错误消息
    let message = exception.message || 'Internal server error';
    
    // 开发环境显示详细错误,生产环境显示友好错误
    const isProduction = process.env.NODE_ENV === 'production';
    
    // 记录异常
    this.loggerService.error(
      `Exception occurred: ${message}`,
      exception.stack,
      `Request: ${request.method} ${request.url}`
    );

    // 构建错误响应
    const errorResponse = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: isProduction && status === HttpStatus.INTERNAL_SERVER_ERROR 
        ? 'An internal server error occurred' 
        : message,
      ...(!isProduction && exception.stack && { stack: exception.stack }),
    };

    // 返回错误响应
    response
      .status(status)
      .json(errorResponse);
  }
}
4. 注册全局异常过滤器
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './global-exception.filter';
import { LoggerService } from './logger.service';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 创建日志服务实例
  const loggerService = new LoggerService();
  
  // 注册全局异常过滤器
  app.useGlobalFilters(new GlobalExceptionFilter(loggerService));
  
  await app.listen(3000);
}
bootstrap();
5. 测试错误处理系统

创建测试控制器:

// test.controller.ts
import { Controller, Get, Post, Body, Param, BadRequestException } from '@nestjs/common';
import { BusinessException } from './business.exception';

@Controller('test')
export class TestController {
  @Get('bad-request')
  async testBadRequest() {
    throw new BadRequestException('Invalid request parameters');
  }

  @Get('not-found/:id')
  async testNotFound(@Param('id') id: string) {
    throw new BusinessException(`Resource with id ${id} not found`, 404);
  }

  @Get('server-error')
  async testServerError() {
    throw new Error('Something went wrong');
  }

  @Post('business-error')
  async testBusinessError(@Body() body: any) {
    if (!body.name) {
      throw new BusinessException('Name is required');
    }
    return { message: 'Success' };
  }
}

测试结果

开发环境
  1. Bad Request 错误

    {
      "statusCode": 400,
      "timestamp": "2023-10-01T10:00:00.000Z",
      "path": "/test/bad-request",
      "method": "GET",
      "message": "Invalid request parameters"
    }
  2. Not Found 错误

    {
      "statusCode": 404,
      "timestamp": "2023-10-01T10:00:00.000Z",
      "path": "/test/not-found/123",
      "method": "GET",
      "message": "Resource with id 123 not found"
    }
  3. Server Error 错误

    {
      "statusCode": 500,
      "timestamp": "2023-10-01T10:00:00.000Z",
      "path": "/test/server-error",
      "method": "GET",
      "message": "Something went wrong",
      "stack": "Error: Something went wrong\n    at TestController.testServerError..."
    }
生产环境
  1. Server Error 错误(生产环境):
    {
      "statusCode": 500,
      "timestamp": "2023-10-01T10:00:00.000Z",
      "path": "/test/server-error",
      "method": "GET",
      "message": "An internal server error occurred"
    }

代码解析

  1. 自定义异常

    • 创建了BusinessException类,用于处理业务逻辑错误
    • 允许自定义错误消息和状态码
  2. 日志服务

    • 创建了LoggerService类,用于记录异常信息
    • 支持不同级别的日志(error, warn, info, debug)
  3. 全局异常过滤器

    • 捕获所有类型的异常
    • 根据环境变量区分错误处理方式
    • 记录详细的异常信息
    • 返回统一的错误响应格式
  4. 测试验证

    • 创建了测试控制器,模拟不同类型的错误
    • 在不同环境下测试错误处理效果

互动思考问题

  1. 思考:异常过滤器、中间件和守卫(Guards)的区别是什么?它们各自的使用场景是什么?

  2. 讨论:在生产环境中,为什么不应该向客户端返回详细的错误信息?

  3. 实践:尝试创建一个基于角色的异常过滤器,根据用户角色返回不同详细程度的错误信息。

  4. 挑战:如何实现异常的国际化处理,根据用户的语言设置返回不同语言的错误消息?

  5. 扩展:了解NestJS的管道(Pipes),思考它与异常过滤器的区别和配合使用的场景。

小结

本集我们学习了NestJS异常过滤器的核心概念和使用方法,包括:

  • 异常过滤器的基本概念和作用
  • 内置异常的使用方法
  • 自定义异常的创建和使用
  • 异常过滤器的创建和注册
  • 全局异常处理的实现
  • 不同环境下的错误处理策略
  • 异常日志记录

通过实践案例,我们创建了一个完整的统一错误处理系统,展示了如何使用异常过滤器捕获和处理应用中的各种异常,提供一致的错误响应格式。异常过滤器是NestJS处理错误的重要组件,它使得错误处理更加集中和可管理。

在下一集中,我们将学习NestJS的管道(Pipes),了解如何使用管道进行数据验证和转换,确保输入数据的合法性。

« 上一篇 NestJS中间件 (Middleware) 下一篇 » NestJS管道 (Pipes)