1. 概述

在本教程中,我们将介绍 Play Framework 默认的错误处理机制,并实现一个自定义的错误处理器。

首先,我们会编写一个返回多种错误的控制器,观察框架是如何自动处理这些错误的;然后,我们将实现一个自定义错误处理器,并分析其局限性。

2. Play Framework 错误类型

在 Play 中,控制器会在以下三种情况下返回错误:

✅ 客户端请求无效,未能通过验证(客户端错误)
✅ 应用代码抛出异常(服务端错误)
✅ 在 Scala 代码中显式返回错误响应(业务相关错误)

3. 控制器错误示例

我们先来准备一个控制器类,用来返回上述三种类型的错误,以便观察默认行为并测试自定义错误处理器。

下面是包含五个方法的控制器类,分别返回不同类型的错误:

class ErrorDemoController @Inject()(
  val controllerComponents: ControllerComponents
) extends BaseController {
  def notFound(): Action[AnyContent] = Action {
    NotFound
  }
  def exception(): Action[AnyContent] = Action {
    throw new RuntimeException("模拟应用内部错误")
    Ok // 只是为了匹配返回类型
  }

  def internalError(): Action[AnyContent] = Action {
    InternalServerError
  }

  def badRequest(): Action[AnyContent] = Action {
    BadRequest
  }
}

接着,在 routes 文件中添加对应的路由映射:

GET     /errors/notfound             controllers.ErrorDemoController.notfound()
GET     /errors/exception           controllers.ErrorDemoController.exception()
GET     /errors/internalerror       controllers.ErrorDemoController.internalError()
GET     /errors/badRequest          controllers.ErrorDemoController.badRequest()

4. 默认错误处理机制

现在我们逐个访问这些接口,看看 Play 是如何处理这些错误的。

4.1. 处理 Scala 异常

访问 http://localhost:9000/errors/exception,可以看到我们在代码中**主动抛出了一个 RuntimeException 来模拟应用内部错误**。

在开发模式下,Play 会返回一个展示源码的错误页面,并高亮出错的那行代码:

play framework error1

⚠️ 当然,将源码暴露给用户是严重的安全风险,在生产环境中,错误页面信息会大幅减少:

play framework error2

如错误提示所示,我们可以在日志中看到具体的错误 ID 和堆栈信息:

! @7ibgcn351 - Internal server error, for (GET) [/errors/exception] ->
 
play.api.UnexpectedException: Unexpected exception[RuntimeException: Pretend that we have an application error.]
    at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:355)
    ...
Caused by: java.lang.RuntimeException: Pretend that we have an application error.
    at controllers.ErrorDemoController.$anonfun$exception$1(ErrorDemoController.scala:13)

4.2. 显式返回 InternalServerErrorNotFoundBadRequest

访问 http://localhost:9000/errors/internalerror,此时我们**在代码中显式返回了 InternalServerError 状态码**,Play 不会展示默认的错误页面,浏览器只会接收到 500 状态码,并显示其默认错误页。

同理,访问 /errors/notfound/errors/badrequest 时,也会收到对应的 404 和 400 状态码,而不会触发错误处理器。

4.3. 真正的 NotFound 页面

当我们访问一个不存在的页面,比如 http://localhost:9000/this_is_not_here,Play 会显示默认的 404 页面(开发模式下):

play framework error3

如果我们希望在页面不存在时跳转到其他页面,就需要自定义错误处理器。

5. 自定义错误处理器

要实现自定义错误处理,我们需要创建一个新的错误处理器类

class CustomErrorHandler extends HttpErrorHandler {
  def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] = {
    if (statusCode == NOT_FOUND) {
      // 仅当访问不存在页面时生效,而不是显式返回 NotFound
      Future.successful(Redirect(routes.ErrorDemoController.noError()))
    } else {
      Future.successful(
        Status(statusCode)("客户端错误")
      )
    }
  }

  def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = {
    Future.successful(
      InternalServerError("服务端错误:" + exception.getMessage)
    )
  }
}

我们希望在页面不存在时跳转到 /errors/noerror 页面,因此需要在控制器中添加新方法:

def noError(): Action[AnyContent] = Action {
  Ok
}

并在 routes 文件中新增路由:

GET /errors/noerror controllers.ErrorDemoController.noerror()

最后,在 application.conf 中配置使用自定义处理器:

play.http.errorHandler = "errors.CustomErrorHandler"

6. 测试自定义错误处理器

访问 http://localhost:9000/errors/exception,页面将不再是红色错误页,而是显示:“服务端错误:模拟应用内部错误”。这说明 onServerError 方法被成功调用。

⚠️ 注意,自定义错误处理器同样不会拦截我们显式返回的错误状态码。访问 /errors/internalerror 时,浏览器依然会收到 500 状态码并显示默认错误页。

测试客户端错误时,访问 /errors/notfound 会返回 404 状态码,浏览器显示“页面未找到”。但访问 /this_is_not_here 时,自定义的 onClientError 会被调用,并跳转到 /errors/noerror 页面

6.1. 编写自动化测试

在编写控制器单元测试时,框架不会使用自定义错误处理器,这是为了确保我们测试的是控制器逻辑,而不是错误处理器。

如果要测试错误处理器本身,需要单独编写单元测试,直接调用 onClientErroronServerError 方法:

class CustomErrorHandlerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting with Eventually {
  "CustomErrorHandler" should {
    "redirect when a page has not been found" in {
      //given
      val objectUnderTest = new CustomErrorHandler()
      val request = FakeRequest(GET, "/fake")
      val statusCode = StatusCodes.NotFound
      val message = ""

      //when
      val responseFuture = objectUnderTest.onClientError(request, statusCode.intValue, message)

      //then
      eventually {
        status(responseFuture) mustBe StatusCodes.SeeOther.intValue
      }
    }
  }
}

7. 总结

本文中我们测试了 Play Framework 的默认错误处理机制,通过自定义错误处理器修改了其行为,并验证了显式返回错误状态码不会触发错误处理器这一特性。

源码地址:GitHub 项目


原始标题:Error Handling in the Play Framework Using Scala