Skip to content

Update Reqwest to 0.13.1#17543

Open
salmonsd wants to merge 26 commits intoastral-sh:mainfrom
salmonsd:update-reqwest-tls
Open

Update Reqwest to 0.13.1#17543
salmonsd wants to merge 26 commits intoastral-sh:mainfrom
salmonsd:update-reqwest-tls

Conversation

@salmonsd
Copy link

@salmonsd salmonsd commented Jan 16, 2026

Summary

This PR improves the TLS experience by upgrading reqwest to 0.13.1 via #17427

It adds support for three TLS backends via a new hidden --tls-backend flag:

  • rustls-webpki — bundled Mozilla roots from webpki-root-certs (default)
  • rustls — platform/system verifier via rustls-platform-verifier
  • native-tls — native system TLS stack

Custom certificates from SSL_CERT_FILE/SSL_CERT_DIR are merged unconditionally into the root store across all backends using reqwest::tls_merge_certs(), ensuring consistent support in corporate or CI setups without backend-specific gating.

The --native-tls flag and UV_NATIVE_TLS env var are retained for compatibility, mapping to the native-tls backend.

Motivation

reqwest 0.13.1 defaults to rustls as its TLS backend w/ platform verification and removes built-in webpki-roots, and moves its default crypto provider to aws-lc instead of ring (increasing the number of cert signature algos supported) to improve TLS experience.

Changes

  • Dependency updates

  • TLS backend selection

    • Hidden --tls-backend flag: rustls-webpki | rustls | native-tls
    • --native-tls preserved (with explicit conflict handling)
    • UV_NATIVE_TLS env var maps to native-tls backend
    • Default: rustls-webpki
  • Certificate handling

    • Load base roots via webpki-root-certs
    • Use reqwest::tls_certs_only to initialize the root store with bundled certs
    • Merge custom certs from SSL_CERT_FILE/SSL_CERT_DIR using tls_merge_certs
    • Merging is applied unconditionally (no backend gating)
    • Reuses reqwest's certificate merging machinery → avoids custom root store or TLS config management
  • Refactoring & cleanup

    • Centralized logic in uv-client/base_client.rs and uv-client/ssl_certs.rs
    • Removed dead test code
    • Added accept-encoding: identity in registry_client.rs where required
  • Documentation

    • Updated certificates.md:
      • Describes new backends + default
      • Recommends usage patterns (e.g. rustls-webpki for consistency, native-tls for proxies)
      • Explains SSL_CERT_* behavior and migration notes
  • Testing

    • 7 new/expanded tests in uv-client/tests/ssl_certs.rs (loading, precedence, all backends)
    • Updated nextest.toml with SSL test profile override
    • Refreshed subcommand snapshots

Trade-offs & Future Work

  • Using webpki-root-certs + tls_certs_only + tls_merge_certs keeps maintenance low and avoids re-implementing root store logic
  • Manual merging adds control and reduces risk of drift from reqwest internals
  • --native-tls retained for smooth transition; long-term plan is deprecation
  • Planned follow-ups:
    • Promote --tls-backend to visible/stable
    • Introduce --system-certs / --no-system-certs aliases (preview)
    • Switch default to rustls (platform verifier) in a future breaking release
    • Deprecate --native-tls and UV_NATIVE_TLS

@zanieb zanieb self-assigned this Jan 16, 2026
@salmonsd
Copy link
Author

Will work on failing tests (apologies)

@musicinmybrain
Copy link
Contributor

We can now also update reqsign to 0.19.0, apache/opendal-reqsign@v0.18.1...v0.19.0, which will remove another user of reqwest 0.12 from the dependency tree.

@salmonsd
Copy link
Author

We can now also update reqsign to 0.19.0, apache/opendal-reqsign@v0.18.1...v0.19.0, which will remove another user of reqwest 0.12 from the dependency tree.

thanks @musicinmybrain, was waiting for the official release!

Only thing to watch for is axoupdater and the update to axoasset (issue here) to also use reqwest 0.13 to be completely resolved.

@salmonsd
Copy link
Author

Hey @zanieb, wanted to check-in and see if y'all have a plan for this or if there's anything I can do to help to get this implemented?

Comment on lines 237 to 248

/// Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
/// Whether to use the native-tls TLS backend instead of rustls [env: UV_NATIVE_TLS=]
///
/// By default, uv loads certificates from the bundled `webpki-roots` crate. The
/// `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv
/// improves portability and performance (especially on macOS).
/// By default, uv uses the rustls TLS backend, which loads certificates from the platform's
/// native certificate store via the `rustls-platform-verifier` crate. This provides a good
/// balance of portability and performance across platforms.
///
/// However, in some cases, you may want to use the platform's native certificate store,
/// especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's
/// included in your system's certificate store.
/// However, in some cases, you may want to use the native-tls backend instead, which uses
/// the platform's native TLS implementation (e.g., SChannel on Windows, Secure Transport on
/// macOS, OpenSSL on Linux).
#[arg(global = true, long, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_native_tls"))]
pub native_tls: bool,
Copy link
Member

@zanieb zanieb Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm this is a bit different than what I'd expect. This means that

  1. We now use system certificates by default
  2. There's no way to for users to recover using webpki-riots

Right?

For users, --native-tls was a way to opt-in to reading certificates from the system. I think we'll probably need to make more changes here.

I presume there's a way to recover the webpki-roots behavior?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ideal rollout plan for us is probably something like

Here:

  • Upgrade to the latest reqwests version
  • Continue using webpki-roots by default
  • Add --tls-backend <native-tls|rusttls> to allow choosing the tls backend

Next:

  • Add rusttls preview feature which uses rusttls instead of native-tls

Next (breaking):

  • Stabilize using rusttls by default

Next:

  • Add system-certs-default preview feature which uses system certificates instead of webpki roots
  • Deprecate --native-tls in favor of --system-certs and --no-system-certs

Next (breaking):

  • Stabilize the preview behavior

Copy link
Author

@salmonsd salmonsd Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can look into this more. Looks like webpki-roots is an optional dependency in 0.13.1 but they were removed here in a subsequent commit that hasn't been released yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into it a bit too. We can just depend on webpki-roots directly, I presume?

It's unfortunate the rollout needs to be so many steps but alas that's the way things need to be since we're so widely used.

Copy link
Author

@salmonsd salmonsd Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, we can totally depend on it directly. And totally understand attempting to keep as much existing functionality as possible while allowing for incremental improvements.

So here's what I have as immediate requirements:

  • depend on webpki-roots directly and support it as default
  • keep existing --native-tls flag?
  • add new --tls-backend (hidden?) flag (add support for conflicts_with)
    • that flag can flex between native-tls and rustls w/ rustls-platform-verifier

Good news, implementation should be manageable (more so than I originally thought). After reviewing the release notes for reqwest, we have the following:

rustls roots features removed, rustls-platform-verifier is used by default.
To use different roots, call tls_certs_only(your_roots)

I'll work to get this implemented.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch that. tls_certs_only() would only work with webpki-root-certs (full X.509 certs) vs webpki-roots (TrustAnchor objects).

So we'll go the route of implementing a rustls config:

use rustls::RootCertStore;

// Create a root certificate store, webpki-roots into the store
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());

// Build a rustls ClientConfig with the root store
let mut tls_config = rustls::ClientConfig::builder()
    .with_root_certificates(root_store)

// Use the preconfigured TLS config with reqwest
let client = reqwest::Client::builder()
    .tls_backend_preconfigured(tls_config)
    .build()?;

Here's the sample webpki-root-certs implementation:

let certs: Result<Vec<reqwest::Certificate>, _> = webpki_root_certs::TLS_SERVER_ROOT_CERTS
    .iter()
    .map(|cert_der| reqwest::Certificate::from_der(cert_der))
    .collect();

let certs = certs?;

let client = reqwest::Client::builder()
    .tls_backend_rustls()
    .tls_certs_only(certs)
    .build()?;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zanieb ended up going the tls_certs_only() route. I've made comments throughout, let me know what you think.

@zanieb zanieb mentioned this pull request Feb 4, 2026
@konstin
Copy link
Member

konstin commented Feb 9, 2026

All dependency updates are merged and published.

@salmonsd
Copy link
Author

rebased onto uv 0.10.1 and this commit: 976a368

will work to implement these suggestions: #17543 (comment)

Comment on lines +657 to +665
TlsBackend::Rustls => {
let client_builder = client_builder.tls_backend_rustls();
// Merge custom certificates for rustls with platform-verifier
if !custom_certs.is_empty() {
client_builder.tls_certs_merge(custom_certs.to_vec())
} else {
client_builder
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to keep this in via TlsBackend since it will be under the hidden flag.

Comment on lines +666 to +683
TlsBackend::RustlsWebpki => {
// Convert webpki-root-certs to reqwest::Certificate objects
let webpki_certs: Vec<Certificate> = webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|cert_der| Certificate::from_der(cert_der).ok())
.collect();

let client_builder = client_builder
.tls_backend_rustls()
.tls_certs_only(webpki_certs);

// Merge custom certificates on top of webpki-root-certs
if !custom_certs.is_empty() {
client_builder.tls_certs_merge(custom_certs.to_vec())
} else {
client_builder
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up using webpki-root-certs here, instead of webpki-roots.

Three reasons:

  1. While slightly larger in size, webpki-root-certs allows to use the new rustls backend, but only with the certs we pass thanks to tls_certs_only. I think this is a better and less complex implementation that still meets our default needs from the previous version.
  2. With webpki-roots, we'd have to pull in rustls, as well, to configure our own RootCertStore to store certs (in addition to auth, cert trust, SNI, ALPN), so that we can use with reqwest's tls_backend_preconfigured method.
  3. Due to tls_backend_preconfigured requiring a TlsConfig, we'd having to parse through the custom cert env vars again to add them to our cert store and handling mTLS inside the building of the root store. While I could have spent some time to find a way a solution, I think this approach is clean.

Comment on lines +1138 to +1142
// Specify identity encoding to prevent double compression from async_http_range_reader and reqwest
headers.insert(
reqwest::header::ACCEPT_ENCODING,
reqwest::header::HeaderValue::from_static("identity"),
);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 5343 to 5350
let script = context.temp_dir.child("term_signal.py");
script.write_str(indoc! {r"
import os
os.kill(os.getpid(), 11)

os.kill(os.getpid(), 15)
"})?;
let status = context.run().arg(script.path()).status()?;
assert_eq!(status.code().expect("a status code"), 139);
assert_eq!(status.code().expect("a status code"), 143);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had some issues with this test hanging. Ran this code in my REPL, where it also hung. Open to suggestions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your system? I've never heard of it hanging

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, it seemed fishy from the jump, but ended up reverting it here after a successful re-run: 27d53ce

use tracing::debug;

use uv_client::{BaseClientBuilder, WrappedReqwestError};
use uv_client::BaseClientBuilder;
Copy link
Author

@salmonsd salmonsd Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal is related to axoupdater (and depedent axoasset) on an older version of reqwest.

Related:

connectivity: Online,
offline: Disabled,
native_tls: false,
tls_backend: RustlsWebpki,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be overkill, but wanted to show new tls_backend setting working.

@salmonsd salmonsd requested review from konstin and zanieb February 18, 2026 04:00
@konstin
Copy link
Member

konstin commented Feb 19, 2026

CI is currently failing because we introduced an openssl dependency.

@konstin konstin added the enhancement New feature or improvement to existing functionality label Feb 19, 2026
@salmonsd
Copy link
Author

CI is currently failing because we introduced an openssl dependency.

I've pushed this commit: salmonsd@93de71f

PR isn't updating for some reason.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or improvement to existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments