request_filter 返回 Ok(true) 却没写响应
主线位置:请求主线第五步,
process_request调request_filter。Ok(true)是「我已经回过响应了,到此为止」,框架接着调logging、把下游连接交回第三步的 keep-alive 循环。Result<bool>两种极性的说明见 ProxyHttp,process_request的各个出口见 process_request。
sequenceDiagram participant C as curl participant P as pingora C->>P: GET 不带 x-token Note over P: request_filter 返回 Ok true Note over P: logging status=None Note over P: finish 后等下一个请求 Note over C: 等第一个响应 Note over C,P: 双方都在等对方
实验 crate exp-pingora/pitb-lab,bin p19_oktrue,脚本 tools/p19.sh,裸 socket 客户端 tools/wait_close.py(debug);M2 Max、macOS 27、rustc 1.99.0,pingora 0.9.0,tokio 钉在 1.53.2(原因见怎么读这一卷)。代理在 19340,上游 python3 -m http.server 19341。
现象
一个最常见的鉴权写法:没有 x-token 就拦下,返回「已处理」:
async fn request_filter(&self, s: &mut Session, _: &mut ()) -> Result<bool> {
if s.req_header().headers.contains_key("x-token") {
return Ok(false);
}
Ok(true) // "I already answered"
}
curl 不带 token(5 秒上限),再带 token 一次,最后用裸 socket 发一个不带 token 的请求,看服务端什么时候回字节或关连接(等 130 秒):
[curl exit 28]
200 [with token]
still open after 130.0s, got 0 bytes
mode=bug
logging /fast status=None err=None
logging /fast status=Some(200) err=None
logging /fast status=None err=None
- curl 退出码 28:5 秒内一个字节都没收到。
- 带 token 的请求正常,问题只在拦截分支。
- 裸 socket 等了 130 秒:没有响应,连接也没关。
- 服务端没有任何错误日志;
logging看到的是一个「没有错误、也没有响应」(status=None)的请求。
出处
process_request 里 request_filter 返回 Ok(true) 的分支:
match self.inner.request_filter(&mut session, &mut ctx).await {
Ok(response_sent) => {
if response_sent {
// TODO: log error
self.inner.logging(&mut session, None, &mut ctx).await;
self.cleanup_sub_req(&mut session);
let mut persistent_settings = HttpPersistentSettings::for_session(&session);
if let Some(uc) = self.inner.persist_connection_context(&session, &ctx) {
persistent_settings.set_user_context(uc);
}
return session
.downstream_session
.finish()
.await
.ok()
.flatten()
.map(|s| ReusedHttpStream::from_reusable_stream(s, persistent_settings));
}
它不看 session.response_written(),直接 finish(),把连接交回去等下一个请求。下一个请求的那次读没有超时:HTTP/1.1 的请求头一读完,respect_keepalive 就把 keep-alive 设成 Some(0)(无限),这个值经 HttpPersistentSettings 带给下一个会话。这正是第一个请求之后,连接永不空闲超时那个坑;两个坑叠在一起,客户端不设超时就永远等下去。
ProxyHttp 一页的动脑筋答案说,等到「服务端的 keep-alive 超时(apps/mod.rs 默认 60 秒)」也会结束;上面 130 秒的实测说明不会:60 秒那个值只管新连接上的第一个请求,读完请求头就被换成了无限。
问: proxy_upstream_filter 返回 Ok(false) 而没写响应时,框架会补一个 502。request_filter 为什么不补?
答: 两处代码确实不同。proxy_upstream_filter 那一支检查 session.response_written().is_none(),是就写一个静态的 BAD_GATEWAY;request_filter 这一支没有这个检查,上面代码里只有一行 // TODO: log error。Ok(true) 的约定是「响应由钩子负责」,框架照约定办事,不替你兜底。
修法
拦下之前先把响应写出去:
async fn request_filter(&self, s: &mut Session, _: &mut ()) -> Result<bool> {
if s.req_header().headers.contains_key("x-token") {
return Ok(false);
}
let mut resp = ResponseHeader::build(401, Some(1))?;
resp.insert_header("Content-Length", "0")?;
s.write_response_header(Box::new(resp), true).await?;
Ok(true) // "I already answered"
}
MODE=fix,裸 socket 只等 3 秒:
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Thu, 29 Oct 2026 06:42:21 GMT
Connection: keep-alive
[curl exit 0]
200 [with token]
0.0s: b'HTTP/1.1 401 Unauthorized\r\nContent-Lengt'
still open after 3.0s, got 109 bytes
- 401 立刻回来,
Connection: keep-alive,客户端可以在同一条连接上带着 token 重试。 write_response_header的第二个参数true表示没有响应体;要带响应体,传false后再write_response_body。- 不在乎复用的话,
s.respond_error(401).await一行就够,它生成空体响应,并且总是Connection: close(错误、重试与故障转移)。 - 另一种写法是返回
Err(Error::explain(HTTPStatus(401), "...")),交给fail_to_proxy写响应,同样会关掉连接,还会打一条 error 日志。
怎么确认没中招
- 搜
request_filter里每一个return Ok(true)/ 末尾的Ok(true):前面同一条路径上要有write_response_header、respond_error或write_response_header_ref。 - 在
logging里加一行:err是None而session.response_written()也是None,就是这个坑(上面日志里的status=None err=None)。 - 照抄
docs/user_guide/errors.md那段示例时注意它的最后一行是Ok(true),校验通过的请求也会落进这个坑(ProxyHttp)。
小结
request_filter的Ok(true)意思是「已经回过响应」,框架不检查,调完logging就把连接交回 keep-alive 循环。- 什么都没写时客户端收不到任何字节;HTTP/1.1 下这条连接也不会因为空闲被关,实测 130 秒后还开着,日志里没有错误。
- 修法:先
write_response_header(或respond_error)再返回Ok(true);拦截请求也可以返回HTTPStatus错误。