Rust in Everything
基于 reqwest 0.13.5

怎么读这一卷

主线位置:三条主线的总览。本页先锁定 reqwest 和它下层 crate 的版本,再把请求、连接、响应三条主线各拆成几步,标出每一步落在哪个函数、哪一页讲它;然后逐个核对 Cargo.toml 的 feature,其中两处会悄悄改变默认行为:打开 native-tls 会把默认 TLS 后端从 rustls 换成 native-tls,打开 hickory-dns 会把默认 resolver 换成 hickory。最后说明本卷的实验环境,以及为什么每个实验客户端都调 .no_proxy()。

flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1["01 全景 再到 04"]
  q -->|读懂实现| all["00 到 09 按顺序"]
  q -->|只关心连接| c["一次请求 再到 03"]
  q -->|要选型| w["为什么是 reqwest"]

锁定版本

仓库tagcommit说明
reqwestv0.13.5de55373(2026-09-08,「v0.13.5」)crates.io 包 reqwest-0.13.5 的 .cargo_vcs_info.json 记的就是它;这个提交只改了 CHANGELOG.md 和 Cargo.toml 的版本号
hyperv1.12.07f67699c(2026-10-06)和 hyper 卷 锁的版本相同
hyper-utilv0.1.2123a8689(2026-09-24)同上;legacy Client、连接池、HttpConnector、系统代理的读取都在这里
towertower-0.5.34b0a6b0(2026-01-12)和 tower 卷 相同;reqwest 只用 retry、timeout、util 三个 feature
tower-httptower-http-0.6.111d082ef(2026-05-18)axum 卷 讲的是 0.7.1,本卷是 0.6.11:reqwest 0.13.5 的 Cargo.toml 写的是 tower-http = "0.6.8",0.7 要到 tag 之后的 aff2ddb 才升上去
rustlsv/0.23.46f04804fa和 rustls 卷 相同;hyper-rustls 0.27.10、rustls-platform-verifier 0.7.1、aws-lc-rs 1.19.0 本卷不读内部

下层版本是一个新项目写 reqwest = "=0.13.5" 时 Cargo.lock 实际解析到的;h2 是 0.4.20,tokio 是 1.54.1,url 是 2.5.8。

reqwest 仓库的默认分支叫 master,不叫 main。git merge-base --is-ancestor v0.13.5 origin/master 为真,tag 之后 master 上有 5 个提交(到 2026-10-20),都没有发版。源码链接一律指向 tag:https://github.com/seanmonstar/reqwest/blob/v0.13.5/<路径>#L<行>,行号是条目开始的那一行。本地仓库的工作区不在 tag 上,读代码要用 git show v0.13.5:src/<文件>。

三条主线

请求主线:一次 client.get(url).send().await

flowchart TB
  send["RequestBuilder send"] --> exec["execute_request"]
  exec --> dec["Decompression"]
  dec --> redir["FollowRedirect"]
  redir --> cookie["CookieService"]
  cookie --> retry["Retry"]
  retry --> hs["HyperService"]
  hs --> hu["legacy Client"]
  hu --> pend["PendingRequest"]
步骤源码讲它的页
1. RequestBuilder::send:构建期间攒下的错误在这里变成 Pending::new_err,否则交给 execute_requestasync_impl/request.rs请求构建
2. Client::execute_request:检查 scheme 和 https_only,补默认头(Accept: */* 等),给走 HTTP 代理的请求加 Proxy-Authorization,拼出 hyper::Request,调用栈顶的服务,再把两个计时器装进 PendingRequestasync_impl/client.rs执行与 Pending
3. 栈的类型是 LayeredService:tower-http 的 Decompression(补 Accept-Encoding)→ FollowRedirect → CookieService → tower 的 Retry → HyperServiceasync_impl/client.rs、cookie.rs、retry.rs、redirect.rs执行与 Pending、重定向、重试、cookie
4. hyper-util Client::request → one_connection_for:池里的空闲连接和新建连接赛跑hyper-util client/legacy/连接池与 HTTP 版本;实现在 hyper 卷的 legacy Client
5. 响应头回来后,PendingRequest::poll 从重定向层的扩展里取最终 URL,用 Response::new 把总超时的计时器移交给响应体async_impl/client.rs、async_impl/response.rs执行与 Pending、超时

连接主线:一条新连接怎么建起来

flowchart TB
  pool["池里没有空闲"] --> svc["ConnectorService call"]
  svc --> px{有代理吗}
  px -->|有| via["connect_via_proxy"]
  px -->|没有| direct["connect_with_maybe_proxy"]
  direct --> dns["DynResolver"]
  dns --> tcp["HttpConnector TCP"]
  tcp --> tls["rustls 握手 ALPN"]
  via --> tls
  tls --> back["交回 hyper-util"]
步骤源码讲它的页
1. Connector 是 hyper-util 的连接器(一个 Service<Uri>);没有 connector_layer 时它就是 ConnectorService 本身,不装箱connect.rs连接器
2. ConnectorService::call:Unix 套接字优先;再逐个问代理 intercept,命中就走 connect_via_proxy,否则 connect_with_maybe_proxyconnect.rs连接器、代理
3. DNS:HttpConnector 用的 resolver 是 DynResolver,里面是 GaiResolver 或 HickoryDnsResolverdns/DNS
4. TCP:hyper-util 的 HttpConnector::call_async,多个地址时按 Happy Eyeballs 依次尝试hyper-util connect/http.rshyper 卷的 HttpConnector
5. TLS:ClientBuilder::build 里拼好的 rustls::ClientConfig 交给 hyper_rustls::HttpsConnector;ALPN 协商出 h2 还是 http/1.1,决定 hyper-util 接着做哪种 HTTP 握手async_impl/client.rs、connect.rs、tls.rsTLS 配置;握手本身见 rustls 卷的 一次握手

响应主线:响应体怎么读完

flowchart TB
  read["bytes text json"] --> body["Response Body"]
  body --> tot["总超时包装"]
  tot --> rd["读超时包装"]
  rd --> dec["解压层"]
  dec --> inc["hyper Incoming"]
  inc --> back["连接回池"]
步骤源码讲它的页
1. Response::bytes、text、json、chunk、bytes_streamasync_impl/response.rs响应体
2. body::response 按有没有总超时、读超时,给 hyper 的 body 套上 0 到 2 层包装async_impl/body.rs超时
3. 解压在 tower-http 的 Decompression 层里做,reqwest 0.13.5 已经没有自己的解码器tower-http decompression/响应体
4. HTTP/1 连接在 hyper 读完整个响应后回池;HTTP/2 连接握手完就进池共享hyper-util client/legacy/响应体、hyper 卷的 连接池
5. text 用 text_with_charset 按 Content-Type 的 charset 解码(charset feature)async_impl/response.rs响应体

一次请求 把三条线在本机上跑一遍,用 tracing 日志对上每一步。

Feature 逐个核对

default = ["default-tls", "charset", "http2", "system-proxy"],default-tls = ["rustls"]。逐个对着 tag 上的 Cargo.toml:

feature打开什么说明
default-tls(默认)rustls0.12 里它是 native-tls,0.13.0 改成 rustls,见下面的冷知识
rustls__rustls-aws-lc-rs、rustls-platform-verifier、__rustlsprovider 是 aws-lc-rs;证书默认交给 rustls-platform-verifier 用系统信任库校验
rustls-no-providerrustls-platform-verifier、__rustls不带 provider。进程里没装默认 provider 就 build,default_rustls_crypto_provider 会 panic,提示先 install_default()
native-tls / native-tls-vendored__native-tls,加 ALPN(__native-tls-alpn)0.13 起默认带 ALPN;不要 ALPN 用 native-tls-no-alpn / native-tls-vendored-no-alpn
http2(默认)h2,hyper / hyper-util / hyper-rustls 的 http2关掉后 ALPN 只报 http/1.1
http3rustls、h3、h3-quinn、quinn不稳定:没有 --cfg reqwest_unstable 时 lib.rs 第 253 行 直接 compile_error!
charset(默认)encoding_rs、mimeResponse::text 按 charset 解码
system-proxy(默认)hyper-util 的 client-proxy-system除环境变量外,还读 macOS / Windows 的系统代理设置
json / form / queryserde 加 serde_json 或 serde_urlencodedform 和 query 是 0.13.0 新拆出来的,默认不开
cookiescookie、cookie_store还要在 builder 上 cookie_store(true) 才生效
gzip / brotli / zstd / deflatetower-http 对应的 decompression-*打开哪个,请求就自动带哪个 Accept-Encoding
blockingfutures-channel、futures-util、tokio/syncreqwest::blocking 模块
multipartmime_guess、futures-util
streamtokio/fs、futures-util、tokio-utilBody::wrap_stream、Response::bytes_stream
hickory-dnshickory-resolver、once_cell打开就是默认 resolver,见下文
socks空代码里按 cfg(feature = "socks") 编进 SOCKS 支持,没有额外依赖

Cargo.toml 里以双下划线开头的 feature(__tls、__rustls、__rustls-aws-lc-rs、__native-tls、__native-tls-alpn)上面有一句注释:「Internal (PRIVATE!) features used to aid testing. Don't rely on these whatsoever. They may disappear at any time.」代码里的 cfg 大多写的是这些内部 feature,读源码时把 __rustls 理解成「编进了 rustls 分支」即可,不要在自己的 Cargo.toml 里用它们。

📝NOTE
冷知识:默认 TLS 在 0.13.0 从 native-tls 换成了 rustls

#2752(1e416d2,2025-12-23,「feat: make Rustls the default TLS provider.」),首次发布于 0.13.0-rc.1 和 0.13.0。改之前 Cargo.toml 是 default-tls = ["dep:hyper-tls", "dep:native-tls-crate", "__tls", "dep:tokio-native-tls"],上面一行注释说它「doesn't enable the 'native-tls' feature」;改之后是 default-tls = ["rustls-tls"],同时把三个 rustls-tls-*-roots feature 的 provider 从 __rustls-ring 换成新加的 __rustls-aws-lc-rs。提交说明只写了「This switches the default TLS provider to Rustls. It keeps the native-tls feature with the same configuration it had before as the default TLS provider」,另外几条是 Windows 上 aws-lc-rs 的构建修补。同一天的 #2891 把根证书统一交给 rustls-platform-verifier,#2900 把 rustls-tls 改名为 rustls,最后才是今天 tag 上的 default-tls = ["rustls"]。本站还在 0.12.28,所以默认走的仍是 native-tls,见 08 在本站中。

会改变默认行为的两个 feature

同时打开 rustls 和 native-tls,默认走 native-tls。 决定默认后端的是 impl Default for TlsBackend:

impl Default for TlsBackend {
    fn default() -> TlsBackend {
        #[cfg(any(
            all(feature = "__rustls", not(feature = "__native-tls")),
            feature = "http3"
        ))]
        {
            TlsBackend::Rustls
        }

        #[cfg(all(feature = "__native-tls", not(feature = "http3")))]
        {
            TlsBackend::NativeTls
        }
    }
}

只要 __native-tls 在(且没开 http3),默认就是 native-tls。默认 feature 已经带着 rustls,所以「加上 native-tls」等于「换成 native-tls」,rustls 还编在二进制里,只是不用了,要用得调 tls_backend_rustls()。实测:同一段代码,reqwest = "=0.13.5" 默认 feature,和再加 features = ["native-tls"],对本机测试服务器(rustls 服务端,支持 TLS 1.2 和 1.3,ALPN h2、http/1.1)各发一个请求,服务端日志:

[testsrv] conn #1 from 127.0.0.1:56238
[testsrv] conn #1 tls ok, TLSv1_3 alpn=Some("h2")
[testsrv] conn #1 req #1 GET https://localhost:56236/ HTTP/2.0
native-tls feature: false  -> HTTP/2.0
[testsrv] conn #1 from 127.0.0.1:56251
[testsrv] conn #1 tls ok, TLSv1_2 alpn=Some("h2")
[testsrv] conn #1 req #1 GET https://localhost:56249/ HTTP/2.0
native-tls feature: true  -> HTTP/2.0

macOS 上 native-tls 用的 Security.framework 只协商到 TLS 1.2;0.13 的 native-tls 默认带 ALPN,所以仍是 HTTP/2。README 的说法与此一致:「By default, Reqwest uses rustls, but when the native-tls feature is enabled it will use the operating system TLS framework」。依赖图里只要有一个 crate 打开了 reqwest 的 native-tls,整个程序的默认后端就跟着变。

打开 hickory-dns,默认 resolver 就是 hickory。 ClientBuilder::new 里写的是 hickory_dns: cfg!(feature = "hickory-dns"),不需要再调 hickory_dns(true);要回到 getaddrinfo 得调 no_hickory_dns()。本卷的实验模板开了这个 feature,所以实验里的 localhost 默认由 hickory 解析,两种 resolver 的差别见 DNS。

实验环境

  • 实验 crate 依赖 reqwest = "=0.13.5",feature 见 卷首;release 构建,计时取多次中的最小值或中位数,页面上写明次数和当时的 uptime 负载。
  • 本机测试服务器是一个共用的小 crate testsrv(hyper 1 服务端,hyper-util 的 auto::Builder):HTTP/1.1、h2c、带 ALPN 的 HTTPS(rcgen 现签一张 CA 和一张 localhost 证书),记录接受的 TCP 连接数、完成的 TLS 握手数、请求数;按查询参数延迟响应头、分块慢慢发响应体、重定向、返回指定状态码、不回响应直接断开。客户端用 add_root_certificate 或 tls_certs_only 信任那张 CA。

每个实验客户端都调 .no_proxy()。 这台机器的 shell 里设了 HTTP_PROXY / HTTPS_PROXY(指向 127.0.0.1:7890)和含 127.0.0.1、localhost 的 NO_PROXY,macOS 系统设置里也开着同一个代理。默认的 ClientBuilder::build 会 proxies.push(ProxyMatcher::system()),它调 hyper-util 的 Matcher::from_system:先读环境变量,再读 macOS 的系统代理。打印 Client::new() 和 .no_proxy() 两个客户端的 Debug:

Client { accepts: Accepts { gzip: true }, proxies: [Matcher { http: Intercept { uri: http://127.0.0.1:7890/ }, https: Intercept { uri: http://127.0.0.1:7890/ }, no: NoProxy { ips: IpMatcher([Address(127.0.0.1), Network(192.168.64.0/24), Address(::1)]), domains: DomainMatcher(["localhost", ".local", ".xmpbinfo.cn"]) } }], referer: true, default_headers: {"accept": "*/*"} }
Client { accepts: Accepts { gzip: true }, referer: true, default_headers: {"accept": "*/*"} }

把六个代理环境变量都 env -u 掉再跑一次,Client::new() 变成:

Client { accepts: Accepts { gzip: true }, proxies: [Matcher { http: Intercept { uri: http://127.0.0.1:7890/ }, https: Intercept { uri: http://127.0.0.1:7890/ } }], referer: true, default_headers: {"accept": "*/*"} }

代理地址来自 macOS 系统设置,但 no 规则没有了。系统设置的例外列表里写着 localhost,可 hyper-util 的 mac::with_system 只读 HTTP / HTTPS 的开关、主机和端口,不读例外列表。这种环境下发往 127.0.0.1 的请求会被交给系统代理。本卷不去验证代理收到请求后做什么,所有客户端一律 .no_proxy();系统代理的细节见 代理。

资料和代码对不上的地方

本卷把 README、CHANGELOG.md、文档注释当资料读,引用前在代码或实验里核实。tag 上对不上的有:

  • lib.rs 里 reqwest::get 的文档说它会在「native TLS backend cannot be initialized」时失败;0.13 的默认后端是 rustls,Client::new 的文档写的是更笼统的「if a TLS backend cannot be initialized」。
  • lib.rs 的「Proxies」一节说「System proxies look in environment variables」;默认的 system-proxy feature 还会读 macOS / Windows 的系统设置(同一文件的 feature 列表里写了「Use Windows and macOS system proxy settings automatically」),而且如上所述,macOS 的例外列表不生效。
  • CHANGELOG.md 的 0.13.0 条目和代码一致:rustls 成为默认后端,provider 默认 aws-lc,rustls-tls 改名为 rustls,根证书交给 rustls-platform-verifier,query / form 变成 feature。

不讲什么

  • HTTP/3:async_impl/h3_client/(840 行)只在 连接池与 HTTP 版本 里读一节源码,不跑。
  • native-tls 分支只讲和 rustls 不同的地方;Security.framework、SChannel、OpenSSL 本身不讲。
  • hyper、hyper-util 的连接池和 HTTP/1、HTTP/2 实现,链到 hyper 卷;tower 的 Service / Layer / Retry,链到 tower 卷;TLS 握手和证书校验,链到 rustls 卷。
  • tests/、examples/ 只在需要时引用。

阅读路线

前置知识

  • HTTP/1.1 的 keep-alive、HTTP/2 的多路复用、TLS 的 ALPN 扩展(客户端在握手里报自己支持的协议,服务端选一个)。
  • Rust 的 Future、Pin、async/await;tokio 运行时。
  • tower 的 Service 和 Layer:见 tower 卷的 Service trait 和 Layer 与 Stack。

小结

  • 锁定 reqwest v0.13.5(de55373),默认分支是 master;下层按新项目实际解析:hyper 1.12.0、hyper-util 0.1.21、tower 0.5.3、tower-http 0.6.11(axum 卷是 0.7.1)、rustls 0.23.46。
  • 请求主线:send → execute_request → 解压、重定向、cookie、重试四层 → HyperService → hyper-util 的池 → PendingRequest → Response。
  • 连接主线:ConnectorService::call 先看代理 → HttpConnector(DynResolver 解析、Happy Eyeballs 连 TCP)→ rustls 握手,ALPN 定 HTTP 版本 → 交回 hyper-util。
  • 响应主线:bytes / text → 超时包装 → 解压层 → hyper Incoming → 连接回池 → 字符集解码。
  • 同开 rustls 和 native-tls 时默认走 native-tls(macOS 上实测 TLS 1.2);打开 hickory-dns 就换掉默认 resolver;双下划线 feature 是内部的。
  • macOS 系统代理的例外列表不生效,实验客户端一律 .no_proxy()。

💬 关联讨论

发起讨论
加载中...