WEBKT

NestJS 中间件的错误处理:优雅地应对同步与异步错误,打造健壮的 API

7 0 0 0

1. 为什么中间件的错误处理很重要?

2. NestJS 中间件错误处理基础

2.1 中间件的基本结构

2.2 同步错误处理

2.3 异步错误处理

3. 错误处理的最佳实践

3.1 不要将内部错误暴露给客户端

3.2 记录错误日志

3.3 使用全局异常过滤器 (可选,但推荐)

3.4 创建自定义错误类

3.5 考虑错误边界 (Error Boundaries)

4. 示例:一个更完善的中间件错误处理

5. 总结

你好,我是老码农!在开发 NestJS 应用时,中间件是处理请求的强大工具,但随之而来的,就是如何优雅地处理中间件中可能出现的各种错误。一个好的错误处理机制不仅能提高 API 的健壮性,还能避免将内部实现细节暴露给客户端,提升用户体验。

本文将深入探讨如何在 NestJS 中间件中进行错误处理,包括同步错误和异步错误的常见处理方式,以及如何将错误信息以友好的方式返回给客户端。我将分享一些最佳实践,帮助你构建更稳定、更易于维护的 NestJS 应用。

1. 为什么中间件的错误处理很重要?

中间件位于请求处理流程的关键位置,负责拦截、处理请求,并在将请求传递给路由处理程序之前执行一系列操作,如身份验证、日志记录、数据转换等。因此,中间件中出现的错误可能直接影响到后续请求处理流程,甚至导致整个应用崩溃。

以下是一些中间件错误可能带来的问题:

  • 信息泄露: 如果未正确处理错误,可能会将敏感的错误信息(如数据库连接字符串、代码路径等)暴露给客户端,带来安全隐患。
  • 用户体验差: 未友好的错误信息会导致用户困惑,甚至无法正常使用应用。
  • 应用不稳定: 未处理的错误可能导致应用崩溃或进入未定义状态,影响用户的使用。
  • 难以调试: 错误信息不清晰,难以定位问题,增加调试难度。

因此,在中间件中建立完善的错误处理机制至关重要。

2. NestJS 中间件错误处理基础

NestJS 提供了一系列机制来帮助我们处理中间件中的错误。首先,我们需要了解中间件的基本结构。

2.1 中间件的基本结构

在 NestJS 中,中间件本质上是一个函数,该函数接收三个参数:

  • req: Express 的 Request 对象,包含客户端发送的请求信息。
  • res: Express 的 Response 对象,用于向客户端发送响应。
  • next: 一个函数,调用 next() 将控制权交给下一个中间件或路由处理程序。如果中间件中发生错误,且没有调用 next(),则请求将“卡”在这里,不会继续处理。

一个简单的中间件示例:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // 确保调用 next(),将请求传递给下一个中间件或路由处理程序
}
}

2.2 同步错误处理

同步错误是指在中间件的同步代码中发生的错误。例如,在中间件中直接调用一个不存在的函数:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class ErrorMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
try {
// 模拟一个同步错误
nonExistentFunction();
} catch (error) {
console.error('同步错误捕获:', error);
// 错误处理逻辑
res.status(500).json({ message: '服务器内部错误' }); // 返回错误响应
// 不调用 next(),阻止请求继续传递
}
}
}

关键点:

  • try...catch 块: 使用 try...catch 块来捕获同步错误。这是处理同步错误最常用的方法。
  • 错误处理逻辑:catch 块中,你可以记录错误日志、处理错误信息,并向客户端发送错误响应。
  • 错误响应: 通过 res.status() 设置 HTTP 状态码,并使用 res.json()res.send() 返回错误信息。不要直接将原始错误对象暴露给客户端,应该返回友好的、结构化的错误信息。
  • 不调用 next() 如果中间件中发生错误,并且你已经处理了该错误并返回了错误响应,则不要调用 next()。这可以阻止请求传递到下一个中间件或路由处理程序,避免不必要的处理。

2.3 异步错误处理

异步错误是指在中间件的异步代码(例如,使用 async/awaitPromise)中发生的错误。例如,在中间件中进行数据库查询:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { getConnection } from 'typeorm'; // 假设使用 TypeORM
@Injectable()
export class AsyncErrorMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
try {
const connection = getConnection();
// 模拟一个异步错误
await connection.query('SELECT * FROM non_existent_table');
} catch (error) {
console.error('异步错误捕获:', error);
// 错误处理逻辑
res.status(500).json({ message: '数据库查询错误' }); // 返回错误响应
// 不调用 next(),阻止请求继续传递
}
}
}

关键点:

  • async/awaitPromise 使用 async/awaitPromise 来处理异步操作。异步操作的错误需要在 try...catch 块中捕获。
  • 与同步错误处理类似: 异步错误处理的整体流程与同步错误处理类似,使用 try...catch 块,记录错误,并向客户端返回错误响应。
  • next(error) 的特殊情况: 在 NestJS 中,如果你的中间件使用了 next(error),NestJS 会将错误传递给全局的异常过滤器进行处理,而不是像 Express 那样。但是,强烈建议避免在中间件中使用 next(error),而是直接在中间件中处理错误并返回错误响应,这样可以更好地控制错误处理流程。

3. 错误处理的最佳实践

以下是一些在 NestJS 中间件中进行错误处理的最佳实践,可以帮助你构建更健壮、更易于维护的应用。

3.1 不要将内部错误暴露给客户端

问题: 直接将原始错误对象(例如数据库错误、代码错误)暴露给客户端,可能会泄露敏感信息,例如数据库连接字符串、代码路径、内部实现细节等,从而带来安全风险。

解决方案: 捕获错误后,不要直接将错误对象发送给客户端。而是应该创建一个友好的、结构化的错误响应,只包含必要的信息,例如错误代码、错误消息等。

示例:

// 错误响应结构
interface ErrorResponse {
statusCode: number;
message: string;
error?: string; // 可选,用于提供更多错误信息,如错误类型
}
@Injectable()
export class ErrorHandlingMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
try {
// ... 你的中间件逻辑 ...
next();
} catch (error) {
console.error('错误捕获:', error);
let errorResponse: ErrorResponse = {
statusCode: 500,
message: '服务器内部错误',
};
if (error instanceof DatabaseError) {
errorResponse = {
statusCode: 500,
message: '数据库操作失败',
error: error.constructor.name, // 错误类型
};
} else if (error instanceof ValidationError) {
errorResponse = {
statusCode: 400,
message: '输入参数验证失败',
error: error.message,
};
} else if (error instanceof CustomError) {
errorResponse = {
statusCode: error.statusCode,
message: error.message,
error: error.name
}
}
res.status(errorResponse.statusCode).json(errorResponse);
}
}
}
// 自定义错误类
class CustomError extends Error {
statusCode: number;
constructor(message: string, statusCode: number, name?: string) {
super(message);
this.statusCode = statusCode;
this.name = name || 'CustomError';
Object.setPrototypeOf(this, CustomError.prototype);
}
}
// 模拟数据库错误
class DatabaseError extends Error {}
// 模拟验证错误
class ValidationError extends Error {}

总结:

  • 错误响应结构: 定义一个统一的错误响应结构,包含状态码、错误消息等。
  • 错误分类: 根据不同的错误类型,构建不同的错误响应。使用 instanceof 检查错误类型,或者定义自定义错误类。
  • 错误信息: 只返回必要的信息,避免暴露敏感信息。例如,只返回错误消息,而不是完整的堆栈跟踪。
  • 状态码: 根据错误类型设置正确的 HTTP 状态码。

3.2 记录错误日志

问题: 错误发生后,如果不记录错误日志,就很难定位问题,修复错误。

解决方案:catch 块中,使用日志库(例如 NestJS 内置的 Logger)记录错误信息,包括错误堆栈跟踪、请求信息等。日志可以帮助你了解错误发生的上下文,从而更容易地找到错误原因。

示例:

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
private readonly logger = new Logger(LoggingMiddleware.name);
use(req: Request, res: Response, next: NextFunction) {
try {
// ... 你的中间件逻辑 ...
next();
} catch (error) {
this.logger.error(`请求处理失败: ${req.method} ${req.url}`, error.stack);
// ... 错误处理逻辑 ...
}
}
}

总结:

  • 日志级别: 根据错误的严重程度,使用不同的日志级别(例如 errorwarninfodebug)。
  • 错误上下文: 在日志中包含尽可能多的错误上下文信息,例如请求方法、URL、请求体、用户 ID 等。
  • 堆栈跟踪: 记录错误堆栈跟踪,可以帮助你定位错误发生的代码位置。
  • 日志存储: 将日志存储到可靠的存储介质中,例如文件、数据库、日志服务等,方便后续分析和监控。

3.3 使用全局异常过滤器 (可选,但推荐)

虽然我们主要讨论中间件中的错误处理,但 NestJS 的全局异常过滤器提供了一种集中处理错误的方式。可以将中间件中的错误传递给全局异常过滤器进行处理,从而实现更统一的错误处理策略。

如何将错误传递给全局异常过滤器?

  • 使用 next(error) 在中间件的 catch 块中,调用 next(error)。NestJS 会将错误传递给全局异常过滤器。

全局异常过滤器的作用:

  • 统一处理: 全局异常过滤器可以拦截所有未被处理的异常,并进行统一处理,例如记录日志、返回友好的错误响应等。
  • 减少代码重复: 可以减少在每个中间件或路由处理程序中重复的错误处理逻辑。
  • 错误转换: 可以将不同类型的错误转换为统一的错误响应格式。

全局异常过滤器示例:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = '服务器内部错误';
let error = 'InternalServerError';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
message = exceptionResponse;
} else {
message = exceptionResponse['message'] || message;
error = exceptionResponse['error'] || error;
}
}
this.logger.error(
`请求处理失败: ${request.method} ${request.url}`, exception.stack,
);
response.status(status).json({
statusCode: status,
message: message,
error: error,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

main.ts 中注册全局异常过滤器:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './all-exceptions.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
bootstrap();

总结:

  • 注册全局异常过滤器:main.ts 中注册全局异常过滤器,使其生效。
  • 捕获所有异常: 全局异常过滤器可以捕获所有未被处理的异常,包括中间件中的异常。
  • 统一处理逻辑: 在全局异常过滤器中,可以统一处理错误,例如记录日志、返回友好的错误响应等。
  • 错误转换: 可以将不同类型的错误转换为统一的错误响应格式。
  • 选择合适的处理方式: 虽然全局异常过滤器很强大,但在中间件中,我更建议直接处理错误并返回响应,这样可以更好地控制错误处理流程。如果你的应用有非常复杂的错误处理需求,可以结合使用全局异常过滤器。

3.4 创建自定义错误类

问题: 使用原生的 Error 对象,无法方便地添加自定义属性,例如错误代码、错误类型等。

解决方案: 创建自定义错误类,继承自 Error,并添加自定义属性。这样可以更方便地处理不同类型的错误,并提供更丰富的错误信息。

示例:

class CustomError extends Error {
statusCode: number;
errorCode: string;
constructor(message: string, statusCode: number, errorCode: string) {
super(message);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.name = this.constructor.name;
Object.setPrototypeOf(this, CustomError.prototype);
}
}
// 使用自定义错误类
@Injectable()
export class CustomErrorMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
try {
// ... 你的中间件逻辑 ...
if (someCondition) {
throw new CustomError('发生了自定义错误', 400, 'ERR_CUSTOM');
}
next();
} catch (error) {
console.error('错误捕获:', error);
if (error instanceof CustomError) {
res.status(error.statusCode).json({
statusCode: error.statusCode,
message: error.message,
errorCode: error.errorCode,
});
} else {
// ... 其他错误处理 ...
}
}
}
}

总结:

  • 继承 Error 自定义错误类应该继承自 Error
  • 添加自定义属性: 在自定义错误类中,添加自定义属性,例如 statusCodeerrorCode 等。
  • 使用自定义错误类: 在中间件中,使用自定义错误类来抛出错误。
  • 错误处理:catch 块中,使用 instanceof 检查错误类型,并根据错误类型进行不同的处理。

3.5 考虑错误边界 (Error Boundaries)

问题: 在某些情况下,即使你使用了完善的错误处理机制,也可能无法完全避免错误导致应用崩溃。例如,在中间件中发生了未知的错误,或者错误发生在异步操作中,导致错误未被捕获。

解决方案: 错误边界是一种在 React 中常用的概念,用于捕获组件树中的错误,防止错误导致整个应用崩溃。虽然 NestJS 没有直接提供错误边界的概念,但我们可以借鉴这种思想,在关键的中间件中设置“错误边界”。

如何实现“错误边界”?

  1. 监控: 监控关键中间件的运行状态,记录错误发生的时间、错误类型、堆栈跟踪等信息。
  2. 重试: 对于一些瞬时错误,例如网络连接超时,可以尝试重试操作。
  3. 熔断: 如果错误持续发生,可以触发熔断机制,停止对该中间件的调用,从而防止错误蔓延。
  4. 降级: 当中间件发生错误时,可以尝试降级处理,例如返回缓存数据、使用默认值等。
  5. 服务健康检查: 实现服务的健康检查机制,定期检查关键中间件的运行状态,及时发现问题,并采取相应的措施。

示例(伪代码):

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class ErrorBoundaryMiddleware implements NestMiddleware {
private readonly logger = new Logger(ErrorBoundaryMiddleware.name);
private errorCount = 0;
private readonly maxErrorCount = 5; // 允许的最大错误次数
use(req: Request, res: Response, next: NextFunction) {
try {
// ... 你的中间件逻辑 ...
next();
} catch (error) {
this.logger.error('错误边界捕获:', error.stack);
this.errorCount++;
if (this.errorCount > this.maxErrorCount) {
// 触发熔断机制,停止对该中间件的调用
this.logger.error('熔断机制触发,停止对该中间件的调用');
res.status(503).json({ message: '服务暂时不可用' });
return; // 阻止请求继续传递
}
// 尝试重试操作
// ...
// 返回友好的错误响应
res.status(500).json({ message: '服务器内部错误' });
} finally {
// 清理资源
// ...
}
}
}

总结:

  • 监控: 监控关键中间件的运行状态,记录错误信息。
  • 重试: 对于瞬时错误,可以尝试重试操作。
  • 熔断: 如果错误持续发生,可以触发熔断机制,停止对该中间件的调用。
  • 降级: 当中间件发生错误时,可以尝试降级处理。
  • 服务健康检查: 实现服务的健康检查机制。

4. 示例:一个更完善的中间件错误处理

以下是一个更完善的中间件错误处理示例,综合了上述最佳实践。

import { Injectable, NestMiddleware, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
// 自定义错误类
class CustomError extends Error {
statusCode: number;
errorCode: string;
constructor(message: string, statusCode: number, errorCode: string) {
super(message);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.name = this.constructor.name;
Object.setPrototypeOf(this, CustomError.prototype);
}
}
@Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
private readonly logger = new Logger(AuthenticationMiddleware.name);
use(req: Request, res: Response, next: NextFunction) {
try {
// 1. 身份验证逻辑(假设需要验证 token)
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new CustomError('未提供身份验证令牌', HttpStatus.UNAUTHORIZED, 'ERR_AUTH_NO_TOKEN');
}
// 2. 验证 token 的有效性(假设有一个 verifyToken 函数)
const decodedToken = this.verifyToken(token);
// 3. 将用户信息存储在 request 对象中,方便后续处理
(req as any).user = decodedToken;
next();
} catch (error) {
// 4. 错误处理
this.handleError(error, req, res);
}
}
private verifyToken(token: string): any {
// 模拟 token 验证,实际项目中需要使用 JWT 或其他认证库
if (token === 'invalid_token') {
throw new CustomError('无效的身份验证令牌', HttpStatus.UNAUTHORIZED, 'ERR_AUTH_INVALID_TOKEN');
}
return { userId: '123', username: 'testuser' };
}
private handleError(error: any, req: Request, res: Response) {
this.logger.error(`身份验证失败: ${req.method} ${req.url}`, error.stack);
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let message = '服务器内部错误';
let errorCode = 'ERR_INTERNAL';
if (error instanceof CustomError) {
statusCode = error.statusCode;
message = error.message;
errorCode = error.errorCode;
} else if (error instanceof HttpException) {
statusCode = error.getStatus();
message = error.message;
errorCode = 'ERR_HTTP';
} else if (error.name === 'TokenExpiredError') {
statusCode = HttpStatus.UNAUTHORIZED;
message = '身份验证令牌已过期';
errorCode = 'ERR_AUTH_TOKEN_EXPIRED';
} else if (error.name === 'JsonWebTokenError') {
statusCode = HttpStatus.UNAUTHORIZED;
message = '无效的身份验证令牌';
errorCode = 'ERR_AUTH_INVALID_TOKEN';
}
res.status(statusCode).json({
statusCode,
message,
errorCode,
timestamp: new Date().toISOString(),
path: req.url,
});
}
}

示例说明:

  1. 自定义错误类: 定义了 CustomError,用于更方便地处理自定义错误。
  2. 身份验证逻辑: 模拟了一个身份验证中间件,用于验证用户身份。
  3. 错误处理: 使用 try...catch 块捕获错误,并在 handleError 方法中处理错误。
  4. 错误分类: 根据不同的错误类型,设置不同的 HTTP 状态码和错误信息。
  5. 日志记录: 使用 Logger 记录错误日志。
  6. 错误响应: 返回友好的、结构化的错误响应,避免暴露敏感信息。
  7. 考虑了 Token 的各种错误情况: 包括未提供 Token, Token 无效, Token 过期等。

5. 总结

在 NestJS 中间件中进行错误处理是一个重要且复杂的任务。本文介绍了中间件错误处理的基础知识,分享了一些最佳实践,并提供了一个更完善的示例。通过遵循这些最佳实践,你可以构建更健壮、更易于维护的 NestJS 应用,并提供更好的用户体验。

关键要点:

  • 不要将内部错误暴露给客户端: 捕获错误后,不要直接将错误对象发送给客户端。返回友好的、结构化的错误信息。
  • 记录错误日志:catch 块中,使用日志库记录错误信息,包括错误堆栈跟踪、请求信息等。
  • 使用全局异常过滤器: 全局异常过滤器可以拦截所有未被处理的异常,并进行统一处理。
  • 创建自定义错误类: 创建自定义错误类,可以更方便地处理不同类型的错误。
  • 考虑错误边界: 在关键中间件中设置“错误边界”,防止错误导致应用崩溃。

希望这篇文章对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言。

祝你编码愉快!

老码农的程序人生 NestJS中间件错误处理APINode.js

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7888