未知路由返回 404,JSON parser 拒绝返回 400,实验 Gate 提前返回 403,这三种响应都带有外层 Filter 加上的头。控制器同步抛出或返回失败 Stage 时,客户端得到 500,却没有执行相同的成功回调。

状态码属于 HTTP 响应;Stage 成功或失败属于异步计算。二者不能混为一谈。一个 400 Result 可以由成功完成的 Stage 传给 Filter,而失败 Stage 在服务器错误处理中被转换为 500,未必再经过 Filter 的 thenApply。

Router 先选 handler,Filter 再包装

DefaultHttpRequestHandler 的普通请求路径,先执行 routeWithFallback,再调用 filterHandler 包装选定的 handler。选不到普通路由时也会构造处理未匹配请求的 handler,因此 Filter 有机会覆盖应用层的 404。

1
2
3
4
5
6
7
RequestHeader
→ routeWithFallback
→ selected handler / fallback handler
→ Filter chain
→ EssentialAction 的请求体 Accumulator
→ Result / failed Stage
→ server adapter

顺序解释了两个容易混淆的现象。Filter 可以检查已经选定的路由信息,但在 Filter 内只改变 RequestHeader 的 path,不会让默认处理器重新选择另一条路由。真正需要改路由选择时,应在选择前的入口处理,或者显式设计自己的 HttpRequestHandler。

未知路由“经过 Filter”也有条件:请求已经进入当前应用处理器、路径处在适用范围,选定 handler 能由该链包装。协议层拒绝、服务器尚未加载应用、开发 web command 或其他 handler 类型都不由一次普通 HTTP 实验覆盖。

官方Java Filter 指南明确区分 Filter 与 Action 组合的范围。选择全局入口时应检查实际 handler 类型,不能从“全局”这个名字推导所有协议和启动失败都受保护。

两层 Filter 怎样折叠

工程按顺序启用 PipelineHeaderFilter 与 PipelineGateFilter。它们只在存在 X-Pipeline-Id 时记录实验事件,原有 TraceFilter 继续负责 00–05 的回归。这样可以把新时序与旧 trace 分开。

默认处理器使用 filters.foldRight(next) 组合,列表靠前者包住后者。本例的 Header 位于 Gate 外层。调用的进入方向与结果回传方向相反:

1
2
3
4
5
header-enter
gate-enter
parser / action / fallback
gate-exit-status
header-exit-status

配置列表与依赖注入创建顺序也不是同一事实。需要检查启用的 filter 列表、组合实现和请求 trace;某个对象先被 injector 构造,不足以证明它在请求中先调用。

Header Filter 通过下一层取得 Stage,再变换 Result:

1
2
3
4
5
6
7
8
9
10
11
12
13
public CompletionStage<Result> apply(
Function<Http.RequestHeader, CompletionStage<Result>> next,
Http.RequestHeader request) {
if (request.headers().get("X-Pipeline-Id").isEmpty()) {
return next.apply(request);
}
String id = request.headers().get("X-Pipeline-Id").orElseThrow();
trace.add(id, "header-enter");
return next.apply(request).thenApply(result -> {
trace.add(id, "header-exit-" + result.status());
return result.withHeader("X-Pipeline-Security", "applied");
});
}

Result.withHeader 返回带新头的结果对象,Stage 将它传给外层;不能调用方法后丢弃返回值,再期望原结果原地变化。请求头也应通过不可变复制传递,不能用共享可变字段临时保存本次请求。

这里的 X-Pipeline-Security 是观测标记。它说明本次成功结果经过了外层变换,不提供真实浏览器安全策略。生产 CSP、代理头信任、CORS 与认证需要分别配置和验证。

提前返回为何省掉 parser

Gate 检查合成头 X-Pipeline-Block: yes。匹配时记录拒绝,直接返回一个已完成的 403 Stage:

1
2
3
4
5
6
7
8
9
10
11
if (request.headers().get("X-Pipeline-Block")
.filter("yes"::equals).isPresent()) {
trace.add(id, "gate-reject");
trace.add(id, "gate-exit-403");
return CompletableFuture.completedFuture(
Results.forbidden("blocked by lab filter"));
}
return next.apply(request).thenApply(result -> {
trace.add(id, "gate-exit-" + result.status());
return result;
});

这一次调用没有执行 next.apply,下层 EssentialAction 的 parser 不会被 Gate 主动启动。外层 Header 仍然取得正常的 403 Result,所以能够继续增加响应头。

本批向 /pipeline/json 发送畸形 JSON {,同时设置 block 头,得到的稳定事件是:

1
2
3
4
5
header-enter
gate-enter
gate-reject
gate-exit-403
header-exit-403

parserCalls 为零,requestBusinessCalls 为零。这比“接口返回 403”多证明了两项事实:应用 parser 没有调用,控制器业务方法没有进入。它仍不测量网络读取、后端缓冲或服务器为复用连接而处理剩余数据的方式。

Gate 位于 Header 内层,因此拒绝响应仍带观测头。如果把两个 Filter 交换,外层 Gate 可以在 Header 尚未调用时返回,拒绝响应就不会获得 Header 的修改。安全头与拒绝规则的排序必须以这些短路路径验收。

Java Filter 的接口为何没有请求体

Java Filter 接收的是 RequestHeader 和返回 Stage 的 next。它不像控制器那样直接取得已经解析的 Http.Request,但整个链仍然必须交给服务器一个消费 ByteString 的 Accumulator。

Java Filter 将接口桥接到 Scala Filter;Filters.scala 用两个 Promise 连接这两种表示。

一个 Promise 提供下一层产生的 Result;另一个提供交给框架的 body Accumulator。Filter 调用 next 时,适配器取得 next(rh) 的 Accumulator,后续消费完成再把 Result 交给前一个 Promise。Filter 自己生成结果、没有调用 next 时,适配器以 Accumulator.done 作为 fallback。

这不是把 body 先无条件读完再执行 Filter。适配器需要先知道 Filter 是否继续,才能确定采用下层 parser 还是直接完成。两个 Promise 对应的是“消费器是否就绪”和“结果是否就绪”两项状态。

EssentialFilter 直接包装 EssentialAction,能够参与更底层的 Accumulator 或流变换;普通 Java Filter 将这种细节收敛为 Stage 接口。若要检查完整 body,必须明确缓冲上限和新的消费流程,不能把字节流读取后假定下一层仍能再次取得相同数据。

请求体读取通常是一条流的消费。全局日志只需记录头与有限元数据时,使用 Filter 即可;需要流级计数、复制或改写时,应在合适的 EssentialFilter/parser 入口处理,并为额外缓冲与取消承担责任。

解析拒绝与失败 Stage 的差别

parser 的 Either.Left(Result) 表示提前生成响应。适配后它仍然可以正常完成结果 Stage,因此 Header、Gate 的 thenApply 能观察 400、413 或 415。

业务同步 throw 和失败 Stage 没有正常 Result。本例两个 Filter 都只提供 thenApply,它们没有失败回调。真实 HTTP 中,Action 的两层失败事件出现,而 gate-exit-500 与 header-exit-500 都没有出现。

场景 客户端状态 外层 header parser/业务 Filter 成功退出
合法 JSON 200 有 均执行 两层
未知路径 404 有 业务不执行 两层
route 参数绑定失败 400 有 业务不执行 两层
畸形 JSON 400 有 parser 执行,业务不执行 两层
Gate 拒绝 403 有 parser 与业务均不执行 两层
同步 throw / failed Stage 500 无实验 header 业务进入后失败 均未执行

这个差别决定了计时和日志应该使用哪一种回调。记录所有 Stage 终态,需要成功与失败都覆盖的观察;只调整正常 Result,可以使用 thenApply。将失败恢复为结果则会改变控制流,必须明确保留的状态和异常信息。

若产品要求所有应用错误响应都有某些安全头,应检查 HttpErrorHandler 与服务器错误转换路径,不能只加一个成功回调就宣布覆盖完成。传输层还可能在应用之外返回错误,保证范围应明确到入口。

Filter 计时的结束点

next 返回的 Stage 在 Result 可用时完成。本批 header-exit 发生在此时;它没有等待流式正文最终发送完。给指标命名为“响应完全结束”,会把计算完成与传输结束混在一起。

日志中至少应保留阶段名称:请求进入、Result 可用、响应流结束以及业务提交。哪个事件用于延迟统计,应由要回答的问题决定。远程业务完成之后也可能发送失败;客户端提前断开也未必停止业务。

Filter 是组合边界,不能自动承担连接池、文件和流资源的所有释放。持有资源的组件应该将终止信号连接到自身清理;仅在 Filter 增加 finally 不足以验证异步资源释放。

验证入口与适用范围

在 play-lab/ 按 README 固定 JDK 与 launcher 后运行:

1
2
3
bash sbtw test stage
python3 lab/pipeline_checks.py --dev
python3 lab/pipeline_checks.py

证据在 evidence/batch06-08/:两种服务器入口各 118 请求通过,trace 和计数均保存。JUnit 另覆盖已知与未知路径的 Gate 拒绝。上游 EnabledFiltersSpec 用于核对启用列表,不代表本批运行了整个上游测试。

要解决的问题 放置位置 容易遗漏的路径
应用请求头记录与正常响应变换 Filter failed Stage 与流终止
在路由选择前重写目标 HttpRequestHandler 等前置入口 fallback 与协议范围
指定业务方法的公共条件 Action 组合 默认 parser 先拒绝
请求体流级操作 EssentialFilter / parser 缓冲上限、单次消费与取消

框架边界明确后,指标和拒绝规则才能使用同一套阶段定义;实际 trace 则用来核验配置是否建立了预期的包装关系。

参考资料与继续阅读

参考 HttpRequestHandler、Java Filter 指南 和 Scala Filter 适配。累计代码可从第 00 篇源码包下载。

上一篇:Action 组合。下一篇:BodyParser。