You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Play Framework:如何在HTTP过滤器中处理服务器错误?

解决方案:处理Play过滤器中的异常场景

好问题!你遇到的核心问题是Play框架中Future[Result]的失败分支未被捕获——当控制器抛出异常时,nextFilter(requestHeader)返回的是一个失败的Future,而你原来的代码只用了map(只处理成功场景),导致异常时日志逻辑完全不执行。我们可以通过transform或recoverWith来覆盖成功和失败两种情况,完美解决这个问题。

一、修复基础访问日志过滤器(LoggingFilter)

把原来的map替换成transform,同时处理成功返回Result和失败抛出Throwable的场景:

class LoggingFilter @Inject() (val mat: Materializer, implicit val ec: ExecutionContext) extends Filter {
  def apply(nextFilter: RequestHeader => Future[Result])
           (requestHeader: RequestHeader): Future[Result] = {
    val startTime = System.currentTimeMillis

    nextFilter(requestHeader).transform(
      // 处理成功返回Result的情况
      result => {
        val endTime = System.currentTimeMillis
        val requestTime = endTime - startTime
        Logger.info(s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}")
        Success(result.withHeaders("Request-Time" -> requestTime.toString))
      },
      // 处理控制器抛出异常的情况
      exception => {
        val endTime = System.currentTimeMillis
        val requestTime = endTime - startTime
        Logger.error(
          s"${requestHeader.method} ${requestHeader.uri} failed after ${requestTime}ms",
          exception // 传入exception,打印完整堆栈信息便于排查
        )
        Failure(exception) // 继续抛出异常,让Play框架生成标准错误响应
      }
    )
  }
}

关键说明:

  • transform方法接受两个函数:第一个处理成功的Result,第二个处理失败的Throwable,确保所有请求场景都被覆盖。
  • 异常分支保留完整堆栈打印,同时继续抛出异常,不干扰Play框架的默认错误处理流程。

二、修复带请求体记录的错误日志过滤器(RequestErrorLogFilter)

这个场景需要确保即使控制器抛异常,之前累积的请求体数据也能被记录。同样用transform替代map,同时在异常分支中复用已收集的请求体:

class RequestErrorLogFilter @Inject()(actorSystem: ActorSystem)(implicit ec: ExecutionContext) extends EssentialFilter {
  private val logger = org.slf4j.LoggerFactory.getLogger("application.AccumulatorFlowFilter")
  private implicit val logging = Logging(actorSystem.eventStream, logger.getName)
  private val maxRequestBodySize = 1024 // 限制最大记录1KB请求体,避免内存溢出

  override def apply(next: EssentialAction): EssentialAction = new EssentialAction {
    override def apply(request: RequestHeader): Accumulator[ByteString, Result] = {
      val collectedBodyParts = ArrayBuffer.empty[ByteString]
      var currentBodySize = 0

      // 构建Flow:截断并收集请求体,同时不影响原始请求流
      val bodyCollectingFlow: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString]
        .map { chunk =>
          val remainingSpace = maxRequestBodySize - currentBodySize
          if (remainingSpace > 0) {
            val chunkToKeep = if (chunk.size > remainingSpace) chunk.slice(0, remainingSpace) else chunk
            collectedBodyParts.append(chunkToKeep)
            currentBodySize += chunkToKeep.size
          }
          chunk // 继续传递原始chunk给后续控制器处理
        }

      val originalAccumulator = next(request)
      val accumulatorWithBodyCollection = originalAccumulator.through(bodyCollectingFlow)

      // 用transform覆盖成功和失败场景
      accumulatorWithBodyCollection.transform(
        result => {
          logger.info(s"Request completed with result: $result")
          // 记录4xx及以上的客户端错误
          if (result.header.status >= 400) {
            val truncatedBody = collectedBodyParts.map(_.utf8String).mkString("")
            logger.warn(
              s"Client error [${result.header.status}] for URI: ${request.uri}\n" +
              s"Truncated request body (max $maxRequestBodySize bytes): $truncatedBody"
            )
          }
          Success(result)
        },
        exception => {
          // 记录控制器抛出的异常及请求体
          val truncatedBody = collectedBodyParts.map(_.utf8String).mkString("")
          logger.error(
            s"Server exception for URI: ${request.uri}\n" +
            s"Truncated request body (max $maxRequestBodySize bytes): $truncatedBody",
            exception
          )
          Failure(exception)
        }
      )
    }
  }
}

关键说明:

  • 在Flow中提前收集截断后的请求体,不管后续处理成功还是失败,都能复用这些数据。
  • transform确保异常场景下也能触发日志逻辑,同时保留请求体、异常堆栈等关键排查信息。
  • 截断逻辑严格限制请求体大小,完全避免内存溢出风险。

额外注意事项

  1. 线程安全:Play的Accumulator流处理是单线程顺序执行的,所以collectedBodyParts和currentBodySize的修改不会有并发问题,无需额外同步。
  2. 覆盖范围:这个方案能覆盖所有请求链路的异常,包括路由找不到(404)、框架内部错误等,不仅仅是控制器抛出的异常。
  3. 依赖注入:确保过滤器类上的@Inject()注解正确,并且注入了ExecutionContext、Materializer等Play 2.6+要求的必要依赖。

内容的提问来源于stack exchange,提问作者Vasily Sulatskov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:38:13