同一个订单接口可能返回 400、413、415 或 500。只看到状态码,无法知道业务方法是否执行:400 可能来自路由参数绑定,也可能来自 JSON 解析;500 可能来自同步抛异常,也可能来自异步结果失败。排查需要把输入、阶段事件和客户端观察对应起来。

当前工程在过滤器入口、控制器入口以及结果 Stage 的成功或失败处分别打点。它没有给每个源码函数加日志,而是用最少的事件回答一个问题:请求已经越过哪些边界,在哪个分支停止?

先找到 handler,再包装过滤器

Play 3.0.6 的默认请求处理器先选择 handler,再为可过滤的 action 包装过滤器。这一点影响读源码的方式:过滤器的执行入口早于控制器,不意味着 Router 对 handler 的选择也发生在过滤器之后。

固定的 DefaultHttpRequestHandler.handlerForRequest 依次调用 routeWithFallback、Handler.applyStages、filterHandler,再处理一次预处理阶段。filterHandler 为应用上下文内的 EssentialAction 包装过滤器;其他种类的 handler 不在这段代码里获得相同处理。

默认解析模式下,可把本批普通 Java 控制器的责任关系画成下面这样:

1
2
3
4
5
6
7
8
9
10
HTTP 后端取得请求头
-> 请求处理器选择 Router handler 或 fallback action
-> 预处理阶段;为 EssentialAction 包装过滤器
-> 执行过滤器入口
-> 执行选中 action 的 body parser
Left(Result) -> 提前结果
Right(body) -> Java Action 组合 -> 控制器
-> Future/CompletionStage 完成或失败
-> 后端转换 Result,发送头并消费 HttpEntity
-> 客户端读到正文结束

图描述当前默认模式,不包含 WebSocket 握手、自定义请求处理器或所有开发模式 web command。开发模式的构建链接可能在常规路由前提供特殊响应,固定请求处理器源码有这个分支;不能将该图推广为每个 Play handler 的通用执行序列。

filters.foldRight(next)(_.apply(_)) 说明过滤器如何包装 action。组合的构建顺序与运行时的进入/返回顺序不同,尤其存在提前返回时。第 07 篇再扩展多层过滤器;这里先用一层可观察过滤器保留错误分支。

parser 返回错误结果,控制器就没有被调用

Scala 的 Either 在这条路径表示两个分支:Left(Result) 提供直接返回的结果;Right(body) 提供已解析的请求体。BodyParser.runParserThenInvokeAction 对两个分支分别处理,只有成功分支构造带 body 的 request 后继续调用 action。

这段模式匹配不意味着所有解析失败都抛异常,也不意味着所有失败都返回 400。不同 parser 可以选用不同状态;实体大小限制与 Content-Type 检查也有独立路径。把 parser 的失败统称为“控制器异常”,会把修复位置推到根本未执行的业务方法。

实验的创建方法明确选择 JSON parser:

1
2
3
4
5
6
@BodyParser.Of(BodyParser.Json.class)
public Result create(Http.Request request) {
String id = request.headers().get("X-Lab-Id").orElse("anonymous");
trace.add(id, "controller");
return created(request.body().asJson());
}

它只原样回显合成 JSON,没有领域校验。合法 JSON 不等于合法订单;这个方法用来隔离解析机制,不作为生产创建接口。

内存解析阈值配置为 play.http.parser.maxMemoryBuffer = 1k。本次用 ASCII 内容构造超过 2 KB 的 JSON,触发 413;这没有证明所有 parser 使用同一阈值。multipart、文件及流解析有不同资源边界,后续章节另做实验。中文字符数也不等于 UTF-8 字节数,测尺寸时必须明确编码与实际字节。

JUnit 中正常与畸形 JSON 的事件如下:

1
2
3
4
5
6
good:filter-enter
good:controller
good:result-available-201

bad:filter-enter
bad:result-available-400

控制器事件在 bad 路径缺席,结果事件仍存在;对应源码的直接结果分支。因此在 controller 中 try/catch JSON 解析错误不能覆盖这个失败,业务方法尚未进入。

deferred parsing 会改变这个边界

Play 3.0.6 已包含 deferred body parsing,默认关闭。配置或路由 modifier 可以让 Java Action 组合在实际解析之前执行;dontDeferBodyParsing 还会影响路由级选择。版本已固定,也仍需要记录这个配置维度。

JavaAction 的 root action 调用 BodyParser.parseBody。已解析时该调用继续传递;被延迟时,它成为进入控制器前实际解析的位置。默认与 deferred 路径共享一部分代码,却没有同一套全部先后顺序。

延迟解析的一个用途,是让某些 Action 组合在读取请求体之前决定是否拒绝请求。它不会自动替代 body 大小限制,也不会证明提前拒绝后所有后端资源都已经释放。当前工程没有开启 deferred,相关分支为 SOURCE_VERIFIED;第 06、08 篇再运行组合与 body 消费对照。本篇不把源码存在该分支记成已执行的实验。

无路由也是一个可返回结果的分支

默认请求处理器在无匹配时提供 404 action。routeWithFallback 还会为 HEAD 尝试 GET handler,但传给实际 handler 的请求仍是 HEAD。

因此 POST /health 没匹配 GET 声明时,当前配置得到 404,并未自动生成 405。这是 Play 默认处理路径的实测,不是在重新定义 HTTP 方法语义。自定义请求处理器或路由可改变返回结果;诊断时要检查实际应用声明。

绑定错误则进入生成 router 的 badRequest 分支,状态为 400,不调用订单控制器。GeneratedRouter.call 对绑定结果执行 fold,错误侧选择 badRequest action。本批真实 HTTP 打点中,无路由与绑定失败仍经过观测过滤器:

输入 客户端结果 观测事件
GET /health 200 filter-enter → result-available-200
GET /missing 404 filter-enter → result-available-404
GET /orders/no 400 filter-enter → result-available-400
POST /orders,畸形 JSON 400 filter-enter → result-available-400
GET /fail 或 /async-fail 500 filter-enter → stage-failed

这些是当前默认链和实验过滤器的结果。它们没有证明任何自定义 filter 都会运行,例如更外层 filter 提前拒绝、请求不在应用 context 或 handler 种类变化,都需要另验。

失败的 Stage 为什么最终仍能成为 500

同步方法 fail() 抛出 IllegalStateException;异步方法 asyncFail() 返回已失败的 CompletableFuture。JavaAction 将同步调用和返回的异步完成纳入 Future 适配路径,固定实现 可看到外层调用与内层 Stage 的连接。

实验过滤器在 whenComplete 中记录失败,在 thenApply 中记录成功的 Result:

1
2
3
4
5
6
return next.apply(request).whenComplete((result, failure) -> {
if (failure != null) trace.add(id, "stage-failed");
}).thenApply(result -> {
trace.add(id, "result-available-" + result.status());
return result.withHeader("X-Lab-Result", "available");
});

whenComplete 不把异常转为成功值,所以后面的 thenApply 在失败路径不执行。真实 HTTP 客户端仍收到 500,是后端外围的错误恢复将失败转换为错误响应;不能因为客户端有 500 就断言 filter 成功回调得到了一个状态为 500 的 Result。

PekkoHttpServer.runAction 有这层恢复。实验中的两个业务失败不带 X-Lab-Result,阶段日志也是失败而非 result-available-500。自定义错误处理器可以改变正文,已经发送响应头后的流失败则不能按同一方式改状态码。

Result 到完整响应还有一段工作

/stream 构造三段文本,用 Pekko Source 节流输出。过滤器成功回调加上 X-Lab-Result: available,客户端先收到头,再等后续正文。生产客户端测得头和完整正文之间仍有约 0.3 秒差距,原始时间随运行环境变化;这里只据此验证两个事件不同,不做性能排名。

这个差距不是数据库提交耗时,也不是业务队列时间。本批没有数据库。后端需要物化并消费实体流,客户端还受到传输与读取影响。后端实体转换 表明发送实体是 Result 转换之后的另一层工作。

重跑 python3 lab/verify.py 可取得这些观测。脚本会通过实验专用 /trace 读事件队列,记录到 http-observations.json,然后关闭服务器。这种无界内存队列只用于有限的本机样本,不能直接当生产审计实现;正式日志需要容量、隐私和异步失败策略。

参考资料与继续阅读

本篇的机制入口是 HttpRequestHandler、Action 与 parser 和 JavaAction。全部链接固定在 3.0.6 对应 commit。

上一篇:sbt 与代码生成。下一篇:类型化路由。