Rust in Everything
基于 pingora-proxy 0.9.0

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 秒那个值只管新连接上的第一个请求,读完请求头就被换成了无限。

📝NOTE
没有蠢问题

问: 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 错误。

💬 关联讨论

发起讨论
加载中...