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确保异常场景下也能触发日志逻辑,同时保留请求体、异常堆栈等关键排查信息。- 截断逻辑严格限制请求体大小,完全避免内存溢出风险。
额外注意事项
- 线程安全:Play的
Accumulator流处理是单线程顺序执行的,所以collectedBodyParts和currentBodySize的修改不会有并发问题,无需额外同步。 - 覆盖范围:这个方案能覆盖所有请求链路的异常,包括路由找不到(404)、框架内部错误等,不仅仅是控制器抛出的异常。
- 依赖注入:确保过滤器类上的
@Inject()注解正确,并且注入了ExecutionContext、Materializer等Play 2.6+要求的必要依赖。
内容的提问来源于stack exchange,提问作者Vasily Sulatskov
相关产品推荐
相关产品推荐

