异常处理

本指南介绍 AdonisJS 应用中的异常处理。你将学习如何:

  • 使用全局异常处理器将错误转换为 HTTP 响应
  • 为特定错误类型自定义错误处理
  • 将错误报告给日志服务
  • 创建带有自包含处理逻辑的自定义异常类
  • 为不同环境配置调试模式和状态页

概述

AdonisJS 中的异常处理提供了一个集中式系统,用于管理 HTTP 请求期间的错误。与其将每个路由处理器和中间件都包裹在 try/catch 块中,不如让错误自然地向上冒泡到全局异常处理器,由它将错误转换为适当的 HTTP 响应。

这种方法使你的代码保持整洁,同时确保整个应用中错误处理的一致性。

全局异常处理器

当你创建一个新的 AdonisJS 项目时,全局异常处理器会在 app/exceptions/handler.ts 中创建。它扩展了基础的 ExceptionHandler 类,并提供了两个主要方法:

  • handle 将错误转换为 HTTP 响应。
  • report 记录错误或将它们发送到监控服务。

以下是默认处理器的样子:

app/exceptions/handler.ts
import app from '@adonisjs/core/services/app'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http'

export default class HttpExceptionHandler extends ExceptionHandler {
  /**
   * 控制是否详细显示带有堆栈跟踪的错误。
   * 在生产环境中自动禁用,以保护敏感信息。
   */
  protected debug = !app.inProduction

  /**
   * 为特定状态码启用自定义 HTML 错误页面。
   * 通常在生产环境中启用以提供更好的用户体验。
   */
  protected renderStatusPages = app.inProduction

  /**
   * 将状态码或范围映射到视图模板。
   * 键可以是特定代码如 '404',或范围如 '500..599'。
   */
  protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
    '404': (error, { view }) => {
      return view.render('pages/errors/not_found', { error })
    },
    '500..599': (error, { view }) => {
      return view.render('pages/errors/server_error', { error })
    },
  }

  /**
   * 将错误转换为面向客户端的 HTTP 响应。
   * 重写以自定义错误响应格式。
   */
  async handle(error: unknown, ctx: HttpContext) {
    return super.handle(error, ctx)
  }

  /**
   * 记录错误或将它们发送到监控服务。
   * 切勿尝试从此方法发送 HTTP 响应。
   */
  async report(error: unknown, ctx: HttpContext) {
    return super.report(error, ctx)
  }
}

内联注释解释了每个属性的用途。我们会在本指南后面详细探讨 debugrenderStatusPagesstatusPages

错误如何流经处理器

当 HTTP 请求期间发生错误时,AdonisJS 会自动捕获它并将其转发给全局异常处理器。让我们看看实际效果。

start/routes.ts
import router from '@adonisjs/core/services/router'
import { Exception } from '@adonisjs/core/exceptions'

router.get('fatal', () => {
  /**
   * 抛出一个带有 500 状态码的异常
   * 以及一个用于识别的自定义错误码
   */
  throw new Exception('Something went wrong', { 
    status: 500, 
    code: 'E_RUNTIME_EXCEPTION' 
  })
})

在开发模式下(启用 debug),访问此路由会显示一个由 Youch 提供支持的格式精美的错误页,显示错误消息、完整堆栈跟踪和请求上下文。

在生产模式下(禁用 debug),同样的错误会返回一个简单的 JSON 或纯文本响应,其中仅包含错误消息,不暴露应用的内部结构。

处理特定错误类型

全局异常处理器的 handle 方法接收所有未处理的错误。你可以检查错误类型,并为特定异常提供自定义处理,同时让其他异常回退到默认行为。

以下是一个使用自定义响应格式处理验证错误的示例。

app/exceptions/handler.ts
import { errors as vineJSErrors } from '@vinejs/vine'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'

export default class HttpExceptionHandler extends ExceptionHandler {
  protected debug = !app.inProduction
  protected renderStatusPages = app.inProduction

  async handle(error: unknown, ctx: HttpContext) {
    /**
     * 使用 instanceof 检查错误是否为 VineJS 验证错误
     * 以安全地识别错误类型
     */
    if (error instanceof vineJSErrors.E_VALIDATION_ERROR) {
      /**
       * 直接将验证消息作为 JSON 返回
       * 状态码为 422 Unprocessable Entity
       */
      ctx.response.status(422).send(error.messages)
      return
    }

    /**
     * 对于所有其他错误,委托给父类
     * 它处理默认的错误转换逻辑
     */
    return super.handle(error, ctx)
  }
}

这种使用 instanceof 检查错误类型并提供自定义处理的模式既强大又灵活。你可以为应用中不同的错误类型添加任意多个条件分支。

以下是如何在路由中使用这种自定义验证错误处理的示例。

start/routes.ts
import router from '@adonisjs/core/services/router'
import { createPostValidator } from '#validators/post'

router.post('posts', async ({ request }) => {
  /**
   * 如果验证失败,VineJS 会抛出 E_VALIDATION_ERROR
   * 它由我们的自定义处理器捕获并返回
   * 带有 422 状态码的验证消息
   */
  await request.validateUsing(createPostValidator)
})

调试模式与 Youch

debug 属性控制是否使用 Youch 显示错误,Youch 是一个错误可视化工具,可创建美观、交互式的错误页面。启用调试模式时,Youch 会显示错误消息、完整堆栈跟踪、请求详情,甚至通过语法高亮显示错误发生处的确切代码。

在生产环境中,应始终禁用调试模式,以防止暴露敏感信息。禁用后,错误会使用内容协商(API 请求为 JSON,其他为纯文本)转换为简单的响应,其中仅包含错误消息而不含实现细节。

默认配置 protected debug = !app.inProduction 会自动为你处理这一点,在开发环境中启用调试模式,在生产环境中禁用它。

状态页

状态页允许你为特定的 HTTP 状态码显示自定义的 HTML 页面。这个功能对于面向用户的应用特别有用,你希望提供带有品牌风格的、有帮助的错误体验,而不是通用的错误消息。

statusPages 属性是一个键值映射,其中键是 HTTP 状态码或范围,值是渲染并返回 HTML 内容的回调函数。该回调接收错误对象和 HTTP 上下文,让你可以完全访问视图渲染和错误详情。

export default class HttpExceptionHandler extends ExceptionHandler {
  protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
    '404': (error, { view }) => {
      return view.render('pages/errors/not_found', { error })
    },
    '500..599': (error, { view }) => {
      return view.render('pages/errors/server_error', { error })
    },
  }
}

使用 Inertia 起步套件时,状态页渲染的是 Inertia 组件而不是 Edge 模板。该回调接收来自 HTTP 上下文的 inertia 对象,你用它来渲染前端页面组件。

export default class HttpExceptionHandler extends ExceptionHandler {
  protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
    '404': (error, { inertia }) => {
      return inertia.render('errors/not_found', { error })
    },
    '500..599': (error, { inertia }) => {
      return inertia.render('errors/server_error', { error })
    },
  }
}

页面路径(如 errors/not_found)映射到 inertia/pages/ 内的文件——例如 inertia/pages/errors/not_found.tsx

状态页仅在 renderStatusPages 属性设置为 true 时才会渲染。默认配置在生产环境中启用它们(app.inProduction),因为自定义错误页能提供更好的用户体验;而在开发环境中则保持禁用,因为详细的 Youch 错误页对调试更有用。

Note

状态页仅适用于接受 HTML 或 Inertia 响应的请求。对于接受 JSON 的 API 请求(例如带有 Accept: application/json 的请求),异常处理器会返回 JSON 错误响应。这意味着 API 起步套件在其异常处理器中不包含 renderStatusPagesstatusPages,因为所有响应都是 JSON。

报告错误

report 方法记录错误或将它们发送到外部监控服务。与 handle 不同,它绝不应发送 HTTP 响应。这是两个应该保持分离的不同关注点。

基础的 ExceptionHandler 类提供了 report 的默认实现,使用 AdonisJS 的 logger 记录错误。你可以重写此方法以添加自定义报告逻辑,例如与错误监控服务集成。

app/exceptions/handler.ts
export default class HttpExceptionHandler extends ExceptionHandler {
  async report(error: unknown, ctx: HttpContext) {
    /**
     * 首先调用父类的 report 方法,以确保
     * 错误使用默认行为被记录
     */
    await super.report(error, ctx)

    /**
     * 在此处添加自定义报告逻辑,例如
     * 发送到 Sentry、Bugsnag 或其他服务
     */
  }
}

向错误报告添加上下文

context 方法允许你定义应该随每个错误报告一同包含的额外数据。这些上下文信息帮助你理解错误发生的环境,使调试变得容易得多。

默认情况下,上下文包含请求 ID(x-request-id 请求头)。你可以重写此方法以包含与你的应用相关的任何其他信息。

app/exceptions/handler.ts
export default class HttpExceptionHandler extends ExceptionHandler {
  protected context(ctx: HttpContext) {
    return {
      /**
       * 包含唯一的请求 ID 以在日志中
       * 追踪这个特定请求
       */
      requestId: ctx.request.id(),
      
      /**
       * 如果可用,添加已认证用户的 ID
       * 以识别哪个用户遇到了错误
       */
      userId: ctx.auth.user?.id,
      
      /**
       * 包含 IP 地址用于安全监控
       * 以及识别错误中的模式
       */
      ip: ctx.request.ip(),
    }
  }
}

每当报告错误时,这些上下文数据都会自动包含在内,从而让你无需手动将此类数据添加到每次报告调用中就能获得关于每个错误环境的丰富信息。

从报告中忽略错误

并非所有错误都需要报告。有些错误,如验证失败或未经授权的访问尝试,是正常应用流程中预期的一部分,不需要记录或监控。你可以使用 ignoreStatusesignoreCodes 属性配置要从报告中排除哪些错误。

app/exceptions/handler.ts
export default class HttpExceptionHandler extends ExceptionHandler {
  /**
   * 不应报告的 HTTP 状态码。
   * 这些通常是客户端错误,并不表示
   * 你的应用存在问题。
   */
  protected ignoreStatuses = [400, 401, 403, 404, 422]

  /**
   * 不应报告的错误码。
   * 这些是针对预期错误条件的
   * 特定于应用的错误码。
   */
  protected ignoreCodes = ['E_VALIDATION_ERROR', 'E_UNAUTHORIZED_ACCESS']
}

基础的 ExceptionHandler 类在报告错误之前会在其 shouldReport 方法中检查这些属性。如果你实现了自定义报告逻辑,必须遵守此检查。

app/exceptions/handler.ts
export default class HttpExceptionHandler extends ExceptionHandler {
  async report(error: unknown, ctx: HttpContext) {
    /**
     * 将错误转换为包含状态码和错误码的
     * 标准化 HTTP 错误格式
     */
    const httpError = this.toHttpError(error)
    
    /**
     * 仅当它通过 shouldReport 检查时才报告错误,
     * 该检查会验证它不在 ignoreStatuses 或 ignoreCodes 中
     */
    if (this.shouldReport(httpError)) {
      // 在此处添加你的自定义报告逻辑
      // 例如:发送到外部监控服务
    }
  }
}

这种方法确保了整个应用中错误过滤的一致性,防止你的日志和监控服务被预期的错误淹没。

自定义异常

自定义异常允许你为应用业务逻辑中的特定错误条件创建专门的错误类。自定义异常扩展了基础的 Exception 类,并可以实现自己的 handlereport 方法,将错误条件及其处理逻辑封装在一个自包含的类中。

创建自定义异常

你可以使用 make:exception 命令创建自定义异常。

node ace make:exception PaymentFailed
CREATE: app/exceptions/payment_failed_exception.ts

这会在 app/exceptions 目录中生成一个新的异常类。下面是一个包含处理和报告逻辑的完整自定义异常的样子。

app/exceptions/payment_failed_exception.ts
import { Exception } from '@adonisjs/core/exceptions'
import { HttpContext } from '@adonisjs/core/http'

export default class PaymentFailedException extends Exception {
  /**
   * 此异常的 HTTP 状态码。
   * 设置为静态属性,以便无需
   * 实例化异常即可访问。
   */
  static status = 400

  /**
   * 通过将异常转换为 HTTP 响应来处理它。
   * 当抛出此异常且未被捕获时,
   * 会自动调用此方法。
   */
  handle(error: this, { response }: HttpContext) {
    return response
      .status(error.constructor.status)
      .send('Unable to process the payment. Please try again')
  }

  /**
   * 报告异常以进行记录和监控。
   * 此方法在 handle() 之前调用,以记录
   * 错误的发生。
   */
  report(error: this, { logger, auth }: HttpContext) {
    logger.error(
      { user: auth.user }, 
      'Payment failed for user %s', 
      auth.user?.id
    )
  }
}

当你在应用中的任何地方抛出这个自定义异常时,AdonisJS 会自动调用其 handle 方法来生成 HTTP 响应,并调用其 report 方法来记录错误。对于实现了这些方法的自定义异常,全局异常处理器会被完全绕过。

何时使用自定义异常

当你需要在整个应用中抛出有意义的、特定于业务逻辑的错时,自定义异常是理想选择。它们对于需要专门处理或报告的错条件特别有用。与在全局异常处理器中处理错误不同,自定义异常将错误条件和处理逻辑封装在一起,使你的错误处理更有条理、更易于维护。

另一方面,全局异常处理器旨在更改整个应用处理异常的默认行为。它是处理横切关注点的正确位置,例如一致地格式化所有 API 错误,或与监控服务集成。

当错误特定于你的领域并且需要唯一处理时,请使用自定义异常。当你需要修改整个应用中某类错误的处理方式时,请使用全局异常处理器。

配置参考

异常处理器类提供了几个控制错误处理行为的配置选项:

debug
boolean

当为 true 时,使用 Youch 显示带有堆栈跟踪的详细错误页面。在生产环境中应为 false。默认值:!app.inProduction

renderStatusPages
boolean

当为 true 时,为配置的状态码渲染自定义 HTML 页面。默认值:app.inProduction

statusPages
Record<StatusPageRange, StatusPageRenderer>

将 HTTP 状态码或范围映射到用于自定义错误页面的视图渲染回调。

ignoreStatuses
number[]

不应通过 report 方法报告的 HTTP 状态码数组。

ignoreCodes
string[]

不应通过 report 方法报告的错误码数组。

另请参阅