Rust in Everything
基于 hyper-util 0.1.21

本站的出站客户端

主线位置:客户端主线。请求 → 字节 → 响应这一段由 reqwest 交给 hyper-util 的 legacy Client,后面就是本卷第 6 章讲的取连接、写请求、读响应。本页看 reqwest 在这条路上加了什么、没加什么,以及本站三处调用各自补了什么。

flowchart TB
  oauth["OAuth:每次回调新建"] --> rq["reqwest::Client"]
  pay["支付:每次请求新建"] --> rq
  llm["LLM:进程内共用一个"] --> rq
  rq --> tt["total timeout:Sleep"]
  tt --> lc["legacy Client"]
  lc --> pool["连接池:90 秒空闲"]
  lc --> conn["reqwest Connector"]
  conn --> px{"系统代理命中?"}
  px -->|是| proxy["连到代理"]
  px -->|否| hc["HttpConnector + TLS"]

本页的站点源码链接都指向本站仓库的提交 89a61b1。那个提交的 Cargo.lock 里 reqwest 只有一个版本 0.12.28,底下是 hyper 1.10.1、hyper-util 0.1.20。reqwest 的源码按「crate + 版本 + 文件」引用;hyper-util 的链接指向本卷的 0.1.21,本页引用的几处在 0.1.20 里行为相同(差别见最后一节)。

三处调用

调用方代码Client 活多久超时
OAuth 回调:换 token、取用户信息handle_callback每次回调新建一个,换 token 和 fetch_profile 共用连接 5 秒,总共 15 秒
支付网关:下单、查单、退款payment/src/host.rs 的 send每个请求新建一个连接 5 秒,总共 15 秒
内容审核的 LLMOpenAiChat::new、AnthropicChat::new构造一次,存在进程级的审核流水线 SHARED 里,管理员重载配置时才换总共 30 秒(DEFAULT_TIMEOUT_SECS),没设连接超时

支付那一处:

async fn send(pr: &PayRequest, label: &str) -> Result<String, PayError> {
  // 必须有超时:对账循环逐单 await,一个挂住的连接会让对账永久停住(Y5)。
  let client = reqwest::Client::builder()
    .connect_timeout(std::time::Duration::from_secs(5))
    .timeout(std::time::Duration::from_secs(15))
    .build()
    .map_err(|e| PayError::Gateway(format!("创建 HTTP 客户端失败: {e}")))?;
  // ...

三处都设了超时,原因在下面两节:hyper 这一层不给单个请求计时,reqwest 默认也不计时。三处里有两处每次新建 Client,后果在再下一节。

reqwest 给 legacy Client 配了什么

reqwest 0.12.28 的 src/async_impl/client.rs,ClientBuilder::build 里构造 hyper-util 的 Client:

        let mut builder =
            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new());
        // ...
        builder.timer(hyper_util::rt::TokioTimer::new());
        builder.pool_timer(hyper_util::rt::TokioTimer::new());
        builder.pool_idle_timeout(config.pool_idle_timeout);
        builder.pool_max_idle_per_host(config.pool_max_idle_per_host);
        // ...
        let hyper_client = builder.build(connector_builder.build(config.connector_layers));

省略的部分是按用户配置转发的 HTTP/2 窗口、keep-alive ping 和几个 HTTP/1 解析宽松选项,默认都不设。连同 reqwest 自己的默认值:

项reqwest 0.12.28 默认落在哪一层
timerTokioTimer只转给 h2_builder,供 HTTP/2 keep-alive ping 用
pool_timerTokioTimer连接池的后台清理任务要有它才启动,见 连接池
pool_idle_timeout90 秒同上;和 hyper-util 的默认值一样
pool_max_idle_per_hostusize::MAX同上
连接器reqwest 自己的 Connector,里面是 HttpConnector<DynResolver> 加 TLS用 build 而不是 build_http,所以不走 hyper-util 给 HttpConnector 设 keepalive 的那段
TCPnodelay 开;TCP keepalive 15 秒、间隔 15 秒、重试 3 次;Linux 上 TCP_USER_TIMEOUT 30 秒连接器
代理auto_sys_proxy: true:读环境变量和系统设置连接器,见下文
connect_timeoutNone连接器
timeout、read_timeoutNonereqwest 自己,在 hyper 外面

和直接用 hyper-util 的默认 Builder 比,最大的差别是 pool_timer:默认 Builder 没有它,过期的空闲连接只在下一次取连接时才丢掉;reqwest 的池子会按 max(90 秒, 90 ms) 的间隔在后台清理。

超时只在 reqwest 那一层

hyper 的客户端没有读超时。client::conn::http1::Builder 没有任何超时选项;legacy Client 的 timer 文档写的是「Provide a timer to be used for h2」,pool_timer 是「Provide a timer to be used for timeouts and intervals in connection pools」。一个请求发出去以后,对端不回,ResponseFuture 就一直 Pending。

reqwest 的三个超时都在 hyper 外面:

  • timeout:文档写「applied from when the request starts connecting until the response body has finished」。实现是一个 tokio Sleep,等响应头时和请求 future 一起 poll,拿到响应后再包进 body(TotalTimeoutBody),读 body 时继续计时。
  • read_timeout:每次读,读到数据就重置。本站没用。
  • connect_timeout:reqwest 的 Connector::call 把整个连接过程包进 with_timeout,DNS、TCP、TLS 握手、经代理的连接都算在内;同一个值也设给了内层 HttpConnector。

下面用一个只收请求、永远不回的上游对比有没有 timeout:

reqwest = { version = "=0.12.28", default-features = false }
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "net", "time", "io-util"] }
use std::time::{Duration, Instant};

use tokio::{io::AsyncReadExt, net::TcpListener};

#[tokio::main]
async fn main() {
  // 一个挂住的上游:收下请求,永远不回
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let url = format!("http://{}/", listener.local_addr().unwrap());
  tokio::spawn(async move {
    loop {
      let (mut s, _) = listener.accept().await.unwrap();
      tokio::spawn(async move {
        let mut buf = [0u8; 1024];
        let _ = s.read(&mut buf).await;
        std::future::pending::<()>().await;
        drop(s);
      });
    }
  });

  let plain = reqwest::Client::builder().no_proxy().build().unwrap();
  let timed = reqwest::Client::builder()
    .no_proxy()
    .connect_timeout(Duration::from_secs(5))
    .timeout(Duration::from_millis(500))
    .build()
    .unwrap();

  let t0 = Instant::now();
  match tokio::time::timeout(Duration::from_secs(3), plain.get(&url).send()).await {
    Ok(r) => println!("no timeout:   {:?}", r.map(|r| r.status())),
    Err(_) => println!("no timeout:   still pending after {}ms", t0.elapsed().as_millis()),
  }

  let t0 = Instant::now();
  match timed.get(&url).send().await {
    Ok(r) => println!("timeout(500ms): {}", r.status()),
    Err(e) => println!(
      "timeout(500ms): after {}ms: {e} (is_timeout={})",
      t0.elapsed().as_millis(),
      e.is_timeout()
    ),
  }
}

两个客户端都用了 .no_proxy(),原因见后文。M2 Max、rustc 1.99.0、--release,连跑 3 次,只有端口号和毫秒数(501–504 ms)不同:

no timeout:   still pending after 3003ms
timeout(500ms): after 503ms: error sending request for url (http://127.0.0.1:59935/) (is_timeout=true)

没有 timeout 的请求在 3 秒时还挂着,是外面套的 tokio::time::timeout 把它放弃的;不套,它会一直挂下去。

📝NOTE
没有蠢问题

问: reqwest 明明给 hyper 的 Client 设了 timer,为什么请求还是不限时?

答: 那个 timer 不是给请求用的。legacy Client::timer 只交给 HTTP/2 的 Builder,用来发 keep-alive ping;pool_timer 只用来清理空闲连接。hyper 没有「等响应最多多久」这个选项,「这个请求最多等多久」只能由调用方在外面套一个 Sleep,reqwest 的 timeout 就是这么做的。服务端的 header_read_timeout 也一样要 timer 才生效,但那是另一个方向的事,见 Executor 与 Timer。

本站这几个数字的来历,写在提交 322b85b「fix(http): time out payment gateway and OAuth requests (Y5)」的说明里:

reqwest has no total timeout by default. The reconcile loop awaits each order's gateway query in turn, so one stalled connection stopped reconciliation for the life of the process without a log line; checkout and login could hang the same way. 5s connect / 15s total.

支付对账是 main.rs 里一个 tokio::spawn 出去的循环,每轮逐单 await 网关查询。其中一单的连接挂住,整个循环就停在那个 await 上,后面的订单再也不会被查。登录和下单是用户请求里的一步,挂住就是浏览器一直转圈。LLM 那一处的 30 秒,注释写的是「LLM 调用普遍较慢,但单次至多 30s;超过则上游有问题」。

每次新建 Client:连接池用不上

OAuth 和支付每次调用都 Client::builder()...build()。reqwest 的文档说 Client 内部持有连接池,「it is advised that you create one and reuse it」,而且它内部已经是 Arc,复制一份很便宜。每次新建,池子也是新的,用完就跟着 Client 一起 drop。

下面这个程序用和支付一样的配置,对一个会打印连接开关的 hyper 服务端各发三次请求:一次共用一个 Client,一次每次新建。

reqwest = { version = "=0.12.28", default-features = false }
hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "net", "time"] }
use std::{
  convert::Infallible,
  sync::atomic::{AtomicUsize, Ordering},
  time::{Duration, Instant},
};

use hyper::{Request, Response, body::Incoming, server::conn::http1, service::service_fn};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;

static CONNS: AtomicUsize = AtomicUsize::new(0);

async fn server() -> String {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let url = format!("http://{}/", listener.local_addr().unwrap());
  let t0 = Instant::now();
  tokio::spawn(async move {
    loop {
      let (s, _) = listener.accept().await.unwrap();
      let n = CONNS.fetch_add(1, Ordering::SeqCst) + 1;
      println!("  {:>4}ms server: conn #{n} open", t0.elapsed().as_millis());
      tokio::spawn(async move {
        let svc = service_fn(|_req: Request<Incoming>| async { Ok::<_, Infallible>(Response::new("ok".to_string())) });
        let _ = http1::Builder::new().serve_connection(TokioIo::new(s), svc).await;
        println!("  {:>4}ms server: conn #{n} closed", t0.elapsed().as_millis());
      });
    }
  });
  url
}

fn client() -> reqwest::Client {
  reqwest::Client::builder()
    .no_proxy()
    .connect_timeout(Duration::from_secs(5))
    .timeout(Duration::from_secs(15))
    .build()
    .unwrap()
}

#[tokio::main]
async fn main() {
  println!("one shared Client (like crates/llm):");
  let url = server().await;
  let shared = client();
  for _ in 0..3 {
    shared.get(&url).send().await.unwrap().text().await.unwrap();
    tokio::time::sleep(Duration::from_millis(100)).await;
  }
  drop(shared);
  tokio::time::sleep(Duration::from_millis(100)).await;

  println!("a new Client per call (like OAuth / payment host.rs):");
  CONNS.store(0, Ordering::SeqCst);
  let url = server().await;
  for _ in 0..3 {
    let c = client();
    c.get(&url).send().await.unwrap().text().await.unwrap();
    tokio::time::sleep(Duration::from_millis(100)).await;
  }
  tokio::time::sleep(Duration::from_millis(100)).await;
}
❗IMPORTANT
动脑筋:两种用法下,服务端各看到几条连接?每条在什么时候关?

每次请求之间隔 100 ms,远小于 90 秒的空闲超时。第一组三次请求共用一个 Client,最后 drop(shared);第二组每次循环新建一个 c,循环体结束时 drop。服务端的 open / closed 会怎么排?

在本站,这件事的代价是每次调用都重新做 DNS、TCP 和 TLS 握手:

  • OAuth 一次回调发两个请求。GitHub(github.com 和 api.github.com)、Google 的两个地址不在同一个主机,连接池按 (scheme, authority) 分组,本来也复用不上;Discord、X 两个地址同主机,两个请求又共用同一个 Client,第一个响应读完、连接回池后,第二个请求才有机会复用它。
  • 支付每个请求一个 Client,下单、查单、退款都是新连接;对账循环一轮查多少单,就握多少次手。
  • LLM 的 Client 活得长,连接能复用,pool_timer 也真的在后台跑。

改成共用的形状很简单:一个 static CLIENT: OnceLock<reqwest::Client>,按现在的超时配置构造一次,调用处 .clone() 或直接借用。超时是 Client 上的配置,共用不影响它。改不改由站点决定;登录和支付的调用频率很低,多出来的握手不大可能成为瓶颈。

测试为什么要 .no_proxy()

reqwest 默认 auto_sys_proxy: true,构造 Client 时加一个 ProxyMatcher::system(),里面是 hyper-util 的 Matcher::from_system:先读环境变量 ALL_PROXY、HTTPS_PROXY、HTTP_PROXY、NO_PROXY(大小写两种),环境变量没给的,再在 macOS 上读系统设置。读系统设置的是 mac::with_system,它只取六个键:HTTPEnable、HTTPProxy、HTTPPort 和对应的三个 HTTPS 键。系统设置里的例外列表(ExceptionsList)和「不代理简单主机名」(ExcludeSimpleHostnames)都没读。排除规则只有 NO_PROXY 一个来源。

所以在一台开了系统代理、shell 之外又没设 NO_PROXY 的 Mac 上(比如从 IDE 里跑测试),发往 127.0.0.1 甚至 localhost 的请求也会交给代理。下面的程序只读配置、不发请求,看 from_system() 对三个地址的判断:

hyper-util = { version = "=0.1.21", features = ["client-proxy", "client-proxy-system"] }
use hyper_util::client::proxy::matcher::Matcher;

fn main() {
  // 只读配置,不发任何请求:reqwest 0.12 默认用的就是 Matcher::from_system()
  let m = Matcher::from_system();
  for dst in ["http://127.0.0.1:8080/", "http://localhost:8080/", "https://api.github.com/"] {
    let hit = m.intercept(&dst.parse().unwrap()).map(|i| i.uri().to_string());
    println!("{dst:<26} -> {hit:?}");
  }
}

测试用的 Mac 上,系统代理是 Clash 默认的 127.0.0.1:7890,系统例外列表里有 localhost;shell 里设了 HTTP_PROXY 和包含 127.0.0.1、localhost 的 NO_PROXY。先在 shell 里直接跑,再用 env -u 去掉全部 8 个代理环境变量跑。每种各跑 3 次,输出相同:

== shell env
http://127.0.0.1:8080/     -> None
http://localhost:8080/     -> None
https://api.github.com/    -> Some("http://127.0.0.1:7890/")
== no proxy env vars
http://127.0.0.1:8080/     -> Some("http://127.0.0.1:7890/")
http://localhost:8080/     -> Some("http://127.0.0.1:7890/")
https://api.github.com/    -> Some("http://127.0.0.1:7890/")

(两个 == 标题是运行时 echo 的。)有 NO_PROXY 时回环地址不走代理;没有环境变量时,连系统例外列表里明写的 localhost 也被交给了代理。

被交给代理以后会怎样,取决于代理。用一个本地的「黑洞代理」模拟最坏的情况:它收下请求、什么也不回就关连接。程序把 HTTP_PROXY 指向它,再分别用 Client::new() 和 .no_proxy() 访问一个本地 axum 服务:

reqwest = { version = "=0.12.28", default-features = false, features = ["system-proxy"] }
axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["rt-multi-thread", "net", "io-util"] }
use std::error::Error;

use axum::{Router, routing::get};
use tokio::{io::AsyncReadExt, net::TcpListener};

fn main() {
  // 一个「代理」:收下请求、打印第一行、什么也不回就关连接
  let proxy = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
  let proxy_url = format!("http://{}", proxy.local_addr().unwrap());
  // 在任何线程启动之前设置;reqwest 构造 Client 时读环境变量
  unsafe { std::env::set_var("HTTP_PROXY", &proxy_url) };

  tokio::runtime::Runtime::new().unwrap().block_on(async move {
    proxy.set_nonblocking(true).unwrap();
    let proxy = TcpListener::from_std(proxy).unwrap();
    tokio::spawn(async move {
      loop {
        let (mut s, _) = proxy.accept().await.unwrap();
        let mut buf = [0u8; 1024];
        let n = s.read(&mut buf).await.unwrap();
        let line = String::from_utf8_lossy(&buf[..n]).lines().next().unwrap_or("").to_string();
        println!("  proxy got: {line}");
        drop(s);
      }
    });

    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let url = format!("http://{}/", listener.local_addr().unwrap());
    tokio::spawn(async move {
      axum::serve(listener, Router::new().route("/", get(|| async { "ok" }))).await.unwrap()
    });

    for (name, client) in [
      ("Client::new()", reqwest::Client::new()),
      ("builder().no_proxy()", reqwest::Client::builder().no_proxy().build().unwrap()),
    ] {
      println!("{name}:");
      match client.get(&url).send().await {
        Ok(res) => println!("  {} {:?}", res.status(), res.text().await.unwrap()),
        Err(e) => {
          println!("  error: {e}");
          let mut src = e.source();
          while let Some(s) = src {
            println!("  caused by: {s}");
            src = s.source();
          }
        }
      }
    }
  });
}

同样先用 env -u 去掉 shell 里的代理变量(否则 NO_PROXY 会放过 127.0.0.1)。连跑 3 次,只有端口号不同:

Client::new():
  proxy got: GET http://127.0.0.1:59396/ HTTP/1.1
  error: error sending request for url (http://127.0.0.1:59396/)
  caused by: client error (SendRequest)
  caused by: connection closed before message completed
builder().no_proxy():
  200 OK "ok"
  • 代理收到的请求行是绝对形式 GET http://127.0.0.1:59396/ HTTP/1.1:连接器报告这是代理连接,legacy Client 就保留完整 URI,见 legacy Client。
  • 错误链的最底层是 hyper 的 IncompleteMessage:连接在响应头之前被关。报错指着本地服务的 URL,真正关连接的却是代理。这个错误的一般成因见 connection closed before message completed。
  • .no_proxy() 把 auto_sys_proxy 关掉,也清空代理列表,请求直连。

本站的测试都这么做。openai.rs 的注释写着约定的出处:

    // 项目约定(CLAUDE.md::Rust HTTP Testing):测试场景需用 `.no_proxy()`
    // 避免本地代理拦截 loopback;生产环境保持默认 client 行为。
    // 这里在测试时由 [`Self::with_client`] 注入 `.no_proxy()` 的 client。

with_client 的文档说得更直接:「避免 macOS 系统代理把 127.0.0.1 转给 Clash 等代理黑洞」。OAuth 的 fetch_profile_rejects_error_status 测试也是 reqwest::Client::builder().no_proxy().build()。项目约定里还写着「localhost 不能替代 .no_proxy()」,实测原因就是上面那条:系统例外列表根本没被读。

生产代码不需要 .no_proxy()。容器是 Linux,from_system 在 Linux 上只看环境变量,docker-compose.yml 没给 app 设任何代理变量,请求直连。

📝NOTE
冷知识:reqwest 的系统代理读取,是从 reqwest 搬进 hyper-util 的

hyper-util#189「feat(client): add macOS system proxy support for Matcher」,2025-05-05 合并,首次发布在 hyper-util 0.1.12。PR 描述只有一句:「The added code is mostly ported from reqwest::proxy.」在那之前,hyper-util 的 Matcher 只读环境变量,读 macOS 系统设置的代码在 reqwest 里;这之后 reqwest 0.12 的 src/proxy.rs 直接 use hyper_util::client::proxy::matcher,系统代理的判断交给 hyper-util。

搬过来的 with_system 就是上面说的那段:只取 HTTP / HTTPS 的开关、主机和端口,不读例外列表。所以本站测试里遇到的代理问题,根源在 hyper-util 的这个函数,表现在 reqwest 上。

本站锁的版本与本卷的差别

本站的 reqwest 0.12.28 解析到 hyper 1.10.1、hyper-util 0.1.20。legacy Client 的 client.rs 在 0.1.20 和 0.1.21 之间只多了几个 HTTP/2 选项的转发方法和 edition 2024 的写法;proxy/matcher.rs 的 macOS 部分没变(改动在 Windows 的通配符处理)。和客户端这条路有关的修复:

版本条目和本站的关系
hyper-util 0.1.21#292 cancel the idle interval once its pool emptiesreqwest 设了 pool_timer,后台清理任务会启动。0.1.20 里池子空了它也不停,每 90 秒醒一次,直到 Client 被 drop;本站只有 LLM 的 Client 活得长,影响就是这一个定时唤醒。机制见 连接池
hyper-util 0.1.21#322 TokioExecutor 不再传播当前 tracing span本站没有直接开 hyper-util 的 tracing,但服务端构建(--features server)里 dioxus-server 0.7.9 依赖 hyper-util 时写了 features = ["full"],full 含 tracing,feature 合并后 reqwest 用的那份 hyper-util 也开着它(cargo tree -e features -i hyper-util 核实,ceffc19 的 Cargo.lock 仍是 0.1.20)。reqwest 0.12.28 用的是 TokioExecutor::new(),所以在 0.1.20 下,连接任务和池的清理任务带着发起请求时的 span:OAuth 回调的请求在 server::auth_callback 里,打开 LLM 审核时评论的请求在 server::post_comment 里。实测见 rt::tracing:0.1.20 下这个 span 要到 Client 被 drop 才关闭。OAuth 每次新建 Client,回调返回后很快就关了;LLM 的 Client 长期存在,第一条评论的 server::post_comment span 会一直开着,之后那个 Client 的连接任务、清理任务的事件都挂在它下面。本站的 subscriber 是 fmt,默认过滤级别 info,没开 FmtSpan::CLOSE,而这些事件是 debug / trace,所以默认配置下日志里看不出差别;RUST_LOG 调到 hyper_util=trace 时才会看到。升到 0.1.21 后不再继承
hyper 1.11.1#4110 evict pooled conn on request-side Connection: close请求带 Connection: close 时,那条连接不再回池
hyper 1.12.0#4208 withdraw a stale want when the client dispatcher takes a request修之前 is_ready() 可能在请求在途时为真,legacy 连接池会把正忙的连接借给下一个请求,后者排在整个响应后面,见 连接上的客户端。本站并发共用一个 Client 的只有 LLM

这些都不改变本页的结论:单个请求不限时,要靠 reqwest 的 timeout;测试要 .no_proxy()。

动脑筋答案

M2 Max、rustc 1.99.0、--release,连跑 3 次,只有毫秒数差一两毫秒,以下是其中一次:

one shared Client (like crates/llm):
     1ms server: conn #1 open
   311ms server: conn #1 closed
a new Client per call (like OAuth / payment host.rs):
     0ms server: conn #1 open
   102ms server: conn #1 closed
   103ms server: conn #2 open
   206ms server: conn #2 closed
   206ms server: conn #3 open
   309ms server: conn #3 closed
  • 共用一个 Client:三次请求只有一条连接。每次请求完,连接回到池里,下一次取出来接着用。它在第 311 ms 关闭,就在 drop(shared) 之后(三轮各睡 100 ms),不是因为空闲超时:池子随 Client 一起 drop,池里的 SendRequest 没了,连接任务跟着结束。
  • 每次新建:三条连接。每条都在约 100 ms 后关闭,也就是那一轮循环结束、c 被 drop 的时刻;下一轮的新 Client 有自己的空池子,只能重新连。

小结

  • 本站的出站 HTTP 全走 reqwest 0.12.28,底下是 hyper-util 的 legacy Client。reqwest 给它配了 TokioExecutor、timer(只给 HTTP/2 ping)、pool_timer、90 秒空闲超时,以及自己的连接器(代理、TLS、TCP 选项)。
  • hyper 的客户端没有请求超时,reqwest 的 timeout、read_timeout、connect_timeout 默认都是 None。OAuth 和支付设了连接 5 秒、总共 15 秒,LLM 总共 30 秒,起因是一个挂住的网关查询让对账循环永久停住。
  • OAuth 和支付每次调用新建 Client,连接池用不上,每次都重新握手;LLM 的 Client 构造一次、长期复用。
  • reqwest 默认读系统代理,hyper-util 的 macOS 实现只读开关、主机、端口,不读例外列表,排除只能靠 NO_PROXY。测试里不 .no_proxy(),回环请求可能被代理拿走,最后以 IncompleteMessage 收场。
  • 本站的 hyper 1.10.1 / hyper-util 0.1.20 和本卷版本在这条路径上行为基本相同;0.1.21 的 #292 让空池子的清理任务退出,#322 让 TokioExecutor 不再把请求的 span 带进后台任务(本站经 dioxus-server 的 full 打开了 tracing,但默认日志级别下看不出差别)。

💬 关联讨论

发起讨论
加载中...