Rust in Everything
基于 reqwest 0.13.5

升级 0.13:does not have that feature

主线位置:不在哪一条主线上,在编译期:Cargo.toml 的 feature 决定哪些方法存在、连接主线最后用哪个 TLS 后端。feature 逐个核对和默认后端的选择规则见 怎么读这一卷;TLS 后端和校验器见 TLS 配置。

flowchart TB
  v["版本改成 0.13"] --> f{"rustls-tls 还在吗"}
  f -->|在| e1["cargo 解析失败"]
  e1 --> fix1["改名 rustls"]
  f -->|不在| c{"用了 query form"}
  fix1 --> c
  c -->|用了| e2["E0599"]
  e2 --> fix2["加 feature"]
  c -->|没用| run["编过了"]
  fix2 --> run
  run --> beh["默认 TLS 和根证书变了"]

实验 crate:exp-reqwest/pita-up012(reqwest = "=0.12.28",features = ["json"])和 exp-reqwest/pita-up013(reqwest = "=0.13.5",三份 Cargo.toml 放在 manifests/),两者编译同一个 main.rs:

let client = reqwest::Client::builder()
  .no_proxy()
  .add_root_certificate(ca)
  .timeout(Duration::from_secs(10))
  .build()
  .unwrap();
let r = client
  .post(format!("{base}/echo"))
  .query(&[("page", "2")])
  .form(&[("q", "rust")])
  .send()
  .await
  .unwrap();

服务端是 testsrv 二进制,单独一个进程(--tls -v,rustls,TLS 1.2 和 1.3,ALPN h2、http/1.1),脚本 pita-up013/run.sh。M2 Max、macOS 27、rustc 1.99.0。

现象

第一道:cargo 解析失败。 0.12 里常见的写法是关掉默认 feature、只开 rustls:default-features = false, features = ["json", "rustls-tls"]。版本号改成 0.13.5,cargo check:

error: failed to select a version for `reqwest`.
    ... required by package `pita-up013 v0.1.0 (.../pita-up013)`
versions that meet the requirements `=0.13.5` (locked to 0.13.5) are: 0.13.5

package `pita-up013` depends on `reqwest` with feature `rustls-tls` but `reqwest` does not have that feature.
help: available features: __native-tls, __native-tls-alpn, __rustls, __rustls-aws-lc-rs, __tls, blocking, brotli, charset, cookies, default, default-tls, deflate, form, gzip, hickory-dns, http2, http3, json, multipart, native-tls, native-tls-no-alpn, native-tls-vendored, native-tls-vendored-no-alpn, query, rustls, rustls-no-provider, socks, stream, system-proxy, zstd

这个错误发生在依赖解析阶段,一行代码都还没编译。依赖图里任何一个 crate 还写着 rustls-tls,整个工作区都过不去。

第二道:E0599。 改成 features = ["json"](默认 feature 打开),编译器报:

error[E0599]: no method named `query` found for struct `RequestBuilder` in the current scope
  --> ../pita-up012/src/main.rs:17:6
   |
15 |     let r = client
   |  ___________-
16 | |     .post(format!("{base}/echo"))
17 | |     .query(&[("page", "2")])
   | |     -^^^^^ method not found in `RequestBuilder`
   | |_____|
   |

加上 query 再编,下一个才冒出来:

error[E0599]: no method named `form` found for struct `RequestBuilder` in the current scope
  --> ../pita-up012/src/main.rs:18:6
   |
15 |     let r = client
   |  ___________-
16 | |     .post(format!("{base}/echo"))
17 | |     .query(&[("page", "2")])
18 | |     .form(&[("q", "rust")])
   | |_____-^^^^
   |
help: there is a method `or` with a similar name
   |
18 -     .form(&[("q", "rust")])
18 +     .or(&[("q", "rust")])
   |

方法链上前一个方法找不到时,类型推不下去,后面的方法不会再报,所以要一个一个修。编译器建议的 .or(...) 只是拼写相近,不能照做。

第三道:编过了,行为变了。 features = ["json", "query", "form"] 编过以后,同一段代码对同一个服务器,两个版本的服务端日志:

== pita-up012 -> HTTP/1.1
POST /echo?page=2 HTTP/1.1
...
[server log]
[testsrv] conn #1 tls ok, TLSv1_2 alpn=None
...
== pita-up013 -> HTTP/2.0
POST https://localhost:18543/echo?page=2 HTTP/2.0
...
[server log]
[testsrv] conn #1 tls ok, TLSv1_3 alpn=Some("h2")

0.12.28 的默认是 native-tls(macOS 上是 Security.framework),只协商到 TLS 1.2,不带 ALPN,于是是 HTTP/1.1;0.13.5 的默认是 rustls,TLS 1.3,ALPN 选中 h2。依赖也换了一批:cargo tree 里 0.12.28 这边是 native-tls、hyper-tls、security-framework,0.13.5 这边是 rustls、hyper-rustls、rustls-platform-verifier、aws-lc-rs,以及要编译 C 代码的 aws-lc-sys(构建机上没有 C 编译器时会失败,见 rustls 卷的 没有 C 编译器)。

原因

都写在 0.13.0 的 CHANGELOG 里,「Breaking changes」一共七条:

  - `rustls` is now the default TLS backend, instead of `native-tls`.
  - `rustls` crypto provider defaults to aws-lc instead of _ring_. (`rustls-no-provider` exists if you want a different crypto provider)
  - `rustls-tls` has been renamed to `rustls`.
  - rustls roots features removed, `rustls-platform-verifier` is used by default.
    - To use different roots, call `tls_certs_only(your_roots)`.
  - `native-tls` now includes ALPN. To disable, use `native-tls-no-alpn`.
  - `query` and `form` are now crate features, disabled by default.
  - Long-deprecated methods and crate features have been removed (such as `trust-dns`, which was renamed `hickory-dns` a while ago).

对着两个 tag 的 Cargo.toml:

0.12.280.13.5来源
default-tls = ["dep:hyper-tls", "dep:native-tls-crate", ...]default-tls = ["rustls"]#2752
rustls-tls = ["rustls-tls-webpki-roots"],provider 是 ring,根证书是编进二进制的 webpki-rootsrustls = ["__rustls-aws-lc-rs", "dep:rustls-platform-verifier", "__rustls"]#2900、#2891
rustls-tls-webpki-roots / -native-roots / -manual-roots 及其 -no-provider没有了#2891
native-tls(无 ALPN)、native-tls-alpnnative-tls(带 ALPN)、native-tls-no-alpnCHANGELOG
query()、form() 无条件存在,serde_urlencoded 是必选依赖#[cfg(feature = "query")]、#[cfg(feature = "form")]#2858
trust-dns、macos-system-configuration(都已标为 deprecated)没有了CHANGELOG

根证书的来源变了,这一条编译器不会告诉你。 0.12 的 rustls-tls 自带一份 Mozilla 根证书(webpki-roots),在哪台机器上都一样。0.13 的 rustls 把校验交给 rustls-platform-verifier:macOS、Windows 上交给系统的证书校验,Linux 上它用 rustls-native-certs 读系统证书目录。读到零张根证书时,Verifier::new 返回「No CA certificates were loaded from the system」,reqwest 在 build() 里把它变成 builder error,而 Client::new 对 build() 的结果是 .expect("Client::new()")。所以一个没装 ca-certificates 的极简 Linux 镜像,0.12 + rustls-tls 能连,0.13 + rustls 在建 Client 时就失败。本机是 macOS,这一条没有实测,依据是 rustls-platform-verifier 0.7.1 的 src/verification/others.rs。

修法

按旧 feature 对照改:

0.12 里写的0.13 里改成
rustls-tls(或 rustls-tls-webpki-roots)rustls;想要和以前一样的固定根证书,自己依赖 webpki-roots,传给 tls_certs_only(...)(#2891 的说明原话:「instead of adding webpki-roots with a feature flag, you should use the webpki-roots crate yourself, and pass them into tls_certs_only(certs)」)
rustls-tls-native-rootsrustls(平台校验器本来就用系统根)
rustls-tls-manual-rootsrustls,加 tls_certs_only(你的根)
rustls-tls-*-no-providerrustls-no-provider,进程启动时自己 install_default()
默认 feature(想继续用 native-tls)加 native-tls;只要它被打开,默认后端就是 native-tls
native-tls-alpnnative-tls
native-tls(想保持不带 ALPN)native-tls-no-alpn
用了 .query() / .form()加 query / form
trust-dnshickory-dns

方法名也有一批改了(use_rustls_tls() 推荐写成 tls_backend_rustls()、danger_accept_invalid_certs 推荐写成 tls_danger_accept_invalid_certs 等),旧名都还在,只是文档上的软弃用,不影响编译。

本站从 0.12.28 升级的具体改动(两行 Cargo.toml)和运行时差异,见 本站的 reqwest。

怎么检查

  • 先查谁在开 feature: cargo tree -e features -i reqwest 列出每个 reqwest feature 是被哪个 crate 打开的;工作区里任何一个成员、任何一个依赖写着 rustls-tls 都要改。
  • 升级后看 TLS 依赖: cargo tree -i native-tls、cargo tree -i aws-lc-sys,确认最后编进来的是你以为的那个后端。
  • 看协议版本: 对自己的服务发一个请求打印 response.version()。HTTPS 从 HTTP/1.1 变成 HTTP/2.0,说明换成了 rustls 加 ALPN;上游的负载均衡、限流如果按连接数算,要重新评估。
  • 在部署镜像里跑一次: 根证书的变化只在目标环境里才看得到,在镜像里建一个 Client 并请求一个真实的 HTTPS 地址。

小结

  • rustls-tls 改名 rustls,旧名在 cargo 解析阶段就报「does not have that feature」。
  • query()、form() 从 0.13 起要单独开 feature,否则 E0599,而且一次只报方法链上的第一个。
  • 编过以后,默认 TLS 从 native-tls 变成 rustls(aws-lc-rs),同一段代码在 macOS 上从 TLS 1.2 + HTTP/1.1 变成 TLS 1.3 + HTTP/2。
  • 0.13 的 rustls 用系统根证书,不再自带 webpki-roots;没有系统根证书的环境要自己用 tls_certs_only 提供。

💬 关联讨论

发起讨论
加载中...