怎么读这一卷
主线位置:三条主线的总览。本页先锁定 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"]
锁定版本
| 仓库 | tag | commit | 说明 |
|---|---|---|---|
| reqwest | v0.13.5 | de55373(2026-09-08,「v0.13.5」) | crates.io 包 reqwest-0.13.5 的 .cargo_vcs_info.json 记的就是它;这个提交只改了 CHANGELOG.md 和 Cargo.toml 的版本号 |
| hyper | v1.12.0 | 7f67699c(2026-10-06) | 和 hyper 卷 锁的版本相同 |
| hyper-util | v0.1.21 | 23a8689(2026-09-24) | 同上;legacy Client、连接池、HttpConnector、系统代理的读取都在这里 |
| tower | tower-0.5.3 | 4b0a6b0(2026-01-12) | 和 tower 卷 相同;reqwest 只用 retry、timeout、util 三个 feature |
| tower-http | tower-http-0.6.11 | 1d082ef(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 才升上去 |
| rustls | v/0.23.46 | f04804fa | 和 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_request | async_impl/request.rs | 请求构建 |
2. Client::execute_request:检查 scheme 和 https_only,补默认头(Accept: */* 等),给走 HTTP 代理的请求加 Proxy-Authorization,拼出 hyper::Request,调用栈顶的服务,再把两个计时器装进 PendingRequest | async_impl/client.rs | 执行与 Pending |
3. 栈的类型是 LayeredService:tower-http 的 Decompression(补 Accept-Encoding)→ FollowRedirect → CookieService → tower 的 Retry → HyperService | async_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_proxy | connect.rs | 连接器、代理 |
3. DNS:HttpConnector 用的 resolver 是 DynResolver,里面是 GaiResolver 或 HickoryDnsResolver | dns/ | DNS |
4. TCP:hyper-util 的 HttpConnector::call_async,多个地址时按 Happy Eyeballs 依次尝试 | hyper-util connect/http.rs | hyper 卷的 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.rs | TLS 配置;握手本身见 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_stream | async_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(默认) | rustls | 0.12 里它是 native-tls,0.13.0 改成 rustls,见下面的冷知识 |
rustls | __rustls-aws-lc-rs、rustls-platform-verifier、__rustls | provider 是 aws-lc-rs;证书默认交给 rustls-platform-verifier 用系统信任库校验 |
rustls-no-provider | rustls-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 |
http3 | rustls、h3、h3-quinn、quinn | 不稳定:没有 --cfg reqwest_unstable 时 lib.rs 第 253 行 直接 compile_error! |
charset(默认) | encoding_rs、mime | Response::text 按 charset 解码 |
system-proxy(默认) | hyper-util 的 client-proxy-system | 除环境变量外,还读 macOS / Windows 的系统代理设置 |
json / form / query | serde 加 serde_json 或 serde_urlencoded | form 和 query 是 0.13.0 新拆出来的,默认不开 |
cookies | cookie、cookie_store | 还要在 builder 上 cookie_store(true) 才生效 |
gzip / brotli / zstd / deflate | tower-http 对应的 decompression-* | 打开哪个,请求就自动带哪个 Accept-Encoding |
blocking | futures-channel、futures-util、tokio/sync | reqwest::blocking 模块 |
multipart | mime_guess、futures-util | |
stream | tokio/fs、futures-util、tokio-util | Body::wrap_stream、Response::bytes_stream |
hickory-dns | hickory-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 里用它们。
#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-proxyfeature 还会读 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/只在需要时引用。
阅读路线
- 只想会用:01 全景 的三页;超时、错误、代理;07 常见坑。
- 想读懂实现:按顺序读。02 讲请求在栈里怎么走,03 讲栈的最底下连接怎么来,04 讲响应怎么回去。
- 只关心连接:一次请求 → 连接器 → DNS → TLS 配置 → 连接池与 HTTP 版本。
- 要选型:reqwest 解决什么问题 → 06 设计取舍。
前置知识
- 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→ 超时包装 → 解压层 → hyperIncoming→ 连接回池 → 字符集解码。 - 同开
rustls和native-tls时默认走 native-tls(macOS 上实测 TLS 1.2);打开hickory-dns就换掉默认 resolver;双下划线 feature 是内部的。 - macOS 系统代理的例外列表不生效,实验客户端一律
.no_proxy()。