Recommend --native-tls on SSL errors (#10605)

## Summary

Closes https://github.com/astral-sh/uv/issues/10574.

## Test Plan

```
❯ SSL_CERT_FILE=a cargo run pip install flask -n
   Compiling uv v0.5.18 (/Users/crmarsh/workspace/uv/crates/uv)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.33s
     Running `target/debug/uv pip install flask -n`
⠦ Resolving dependencies...                                                                                                                                                                                                                     × Failed to fetch: `https://pypi.org/simple/flask/`
  ├─▶ Request failed after 3 retries
  ├─▶ error sending request for url (https://pypi.org/simple/flask/)
  ├─▶ client error (Connect)
  ╰─▶ invalid peer certificate: UnknownIssuer
  help: Consider enabling native TLS support via the `--native-tls` command-line flag
```
This commit is contained in:
Charlie Marsh 2025-01-14 13:17:19 -05:00 committed by GitHub
parent e1e9b0447c
commit 325b060829
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 124 additions and 38 deletions

View file

@ -51,6 +51,11 @@ impl Error {
matches!(err.kind(), std::io::ErrorKind::NotFound)
}
/// Returns `true` if the error is due to an SSL error.
pub fn is_ssl(&self) -> bool {
matches!(&*self.kind, ErrorKind::WrappedReqwestError(.., err) if err.is_ssl())
}
/// Returns `true` if the error is due to the server not supporting HTTP range requests.
pub fn is_http_range_requests_unsupported(&self) -> bool {
match &*self.kind {
@ -260,13 +265,9 @@ impl ErrorKind {
pub struct WrappedReqwestError(reqwest_middleware::Error);
impl WrappedReqwestError {
/// Check if the error chain contains a reqwest error that looks like this:
/// * error sending request for url (...)
/// * client error (Connect)
/// * dns error: failed to lookup address information: Name or service not known
/// * failed to lookup address information: Name or service not known
fn is_likely_offline(&self) -> bool {
let reqwest_err = match &self.0 {
/// Return the inner [`reqwest::Error`] from the error chain, if it exists.
fn inner(&self) -> Option<&reqwest::Error> {
match &self.0 {
reqwest_middleware::Error::Reqwest(err) => Some(err),
reqwest_middleware::Error::Middleware(err) => err.chain().find_map(|err| {
if let Some(err) = err.downcast_ref::<reqwest::Error>() {
@ -279,9 +280,16 @@ impl WrappedReqwestError {
None
}
}),
};
}
}
if let Some(reqwest_err) = reqwest_err {
/// Check if the error chain contains a `reqwest` error that looks like this:
/// * error sending request for url (...)
/// * client error (Connect)
/// * dns error: failed to lookup address information: Name or service not known
/// * failed to lookup address information: Name or service not known
fn is_likely_offline(&self) -> bool {
if let Some(reqwest_err) = self.inner() {
if !reqwest_err.is_connect() {
return false;
}
@ -297,6 +305,26 @@ impl WrappedReqwestError {
}
false
}
/// Check if the error chain contains a `reqwest` error that looks like this:
/// * invalid peer certificate: `UnknownIssuer`
fn is_ssl(&self) -> bool {
if let Some(reqwest_err) = self.inner() {
if !reqwest_err.is_connect() {
return false;
}
// Self is "error sending request for url", the first source is "error trying to connect",
// the second source is "dns error". We have to check for the string because hyper errors
// are opaque.
if std::error::Error::source(&reqwest_err)
.and_then(|err| err.source())
.is_some_and(|err| err.to_string().starts_with("invalid peer certificate: "))
{
return true;
}
}
false
}
}
impl From<reqwest::Error> for WrappedReqwestError {