From 325b0608291495cf112b2f55bf672f2d4cf198b9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 14 Jan 2025 13:17:19 -0500 Subject: [PATCH] Recommend `--native-tls` on SSL errors (#10605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 ``` --- crates/uv-client/src/error.rs | 46 ++++++++++++++---- crates/uv/src/commands/diagnostics.rs | 60 ++++++++++++++++++++++-- crates/uv/src/commands/pip/compile.rs | 2 +- crates/uv/src/commands/pip/install.rs | 4 +- crates/uv/src/commands/pip/sync.rs | 4 +- crates/uv/src/commands/project/add.rs | 2 +- crates/uv/src/commands/project/export.rs | 2 +- crates/uv/src/commands/project/lock.rs | 8 ++-- crates/uv/src/commands/project/remove.rs | 4 +- crates/uv/src/commands/project/run.rs | 13 +++-- crates/uv/src/commands/project/sync.rs | 4 +- crates/uv/src/commands/project/tree.rs | 2 +- crates/uv/src/commands/tool/install.rs | 8 ++-- crates/uv/src/commands/tool/run.rs | 3 +- 14 files changed, 124 insertions(+), 38 deletions(-) diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index b9b5a81c761f..294091889490 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -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::() { @@ -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 for WrappedReqwestError { diff --git a/crates/uv/src/commands/diagnostics.rs b/crates/uv/src/commands/diagnostics.rs index 2d375e6f7201..6ec60dca0932 100644 --- a/crates/uv/src/commands/diagnostics.rs +++ b/crates/uv/src/commands/diagnostics.rs @@ -34,26 +34,37 @@ static SUGGESTIONS: LazyLock> = LazyLock::ne pub(crate) struct OperationDiagnostic { /// The hint to display to the user upon resolution failure. pub(crate) hint: Option, + /// Whether native TLS is enabled. + pub(crate) native_tls: bool, /// The context to display to the user upon resolution failure. pub(crate) context: Option<&'static str>, } impl OperationDiagnostic { + /// Create an [`OperationDiagnostic`] with the given native TLS setting. + #[must_use] + pub(crate) fn native_tls(native_tls: bool) -> Self { + Self { + native_tls, + ..Default::default() + } + } + /// Set the hint to display to the user upon resolution failure. #[must_use] - pub(crate) fn with_hint(hint: String) -> Self { + pub(crate) fn with_hint(self, hint: String) -> Self { Self { hint: Some(hint), - ..Default::default() + ..self } } /// Set the context to display to the user upon resolution failure. #[must_use] - pub(crate) fn with_context(context: &'static str) -> Self { + pub(crate) fn with_context(self, context: &'static str) -> Self { Self { context: Some(context), - ..Default::default() + ..self } } @@ -106,6 +117,12 @@ impl OperationDiagnostic { Some(pip::operations::Error::Requirements(err)) } } + pip::operations::Error::Resolve(uv_resolver::ResolveError::Client(err)) + if !self.native_tls && err.is_ssl() => + { + native_tls_hint(err); + None + } err => Some(err), } } @@ -236,6 +253,41 @@ pub(crate) fn no_solution_hint(err: uv_resolver::NoSolutionError, help: String) anstream::eprint!("{report:?}"); } +/// Render a [`uv_resolver::NoSolutionError`] with a help message. +pub(crate) fn native_tls_hint(err: uv_client::Error) { + #[derive(Debug, miette::Diagnostic)] + #[diagnostic()] + struct Error { + /// The underlying error. + err: uv_client::Error, + + /// The help message to display. + #[help] + help: String, + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.err) + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.err.source() + } + } + + let report = miette::Report::new(Error { + err, + help: format!( + "Consider enabling use of system TLS certificates with the `{}` command-line flag", + "--native-tls".green() + ), + }); + anstream::eprint!("{report:?}"); +} + /// Format a [`DerivationChain`] as a human-readable error message. fn format_chain(name: &PackageName, version: Option<&Version>, chain: &DerivationChain) -> String { /// Format a step in the [`DerivationChain`] as a human-readable error message. diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index e2398c44d20d..6d73be2d9cbe 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -401,7 +401,7 @@ pub(crate) async fn pip_compile( { Ok(resolution) => resolution, Err(err) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 6110d43a1bff..03b9593fef5d 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -428,7 +428,7 @@ pub(crate) async fn pip_install( { Ok(graph) => Resolution::from(graph), Err(err) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -462,7 +462,7 @@ pub(crate) async fn pip_install( { Ok(_) => {} Err(err) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index aa85bc89a77c..b90c2448cf27 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -373,7 +373,7 @@ pub(crate) async fn pip_sync( { Ok(resolution) => Resolution::from(resolution), Err(err) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -407,7 +407,7 @@ pub(crate) async fn pip_sync( { Ok(_) => {} Err(err) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 0191c3a0745c..838fbbc1d0ff 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -632,7 +632,7 @@ pub(crate) async fn add( let _ = snapshot.revert(); } match err { - ProjectError::Operation(err) => diagnostics::OperationDiagnostic::with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green())) + ProjectError::Operation(err) => diagnostics::OperationDiagnostic::native_tls(native_tls).with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green())) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())), err => Err(err.into()), diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 64b70d466915..e60ede5b7e8b 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -209,7 +209,7 @@ pub(crate) async fn export( { Ok(result) => result.into_lock(), Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index f33e24d29131..e9612fe4787c 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -193,9 +193,11 @@ pub(crate) async fn lock( Ok(ExitStatus::Success) } - Err(ProjectError::Operation(err)) => diagnostics::OperationDiagnostic::default() - .report(err) - .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())), + Err(ProjectError::Operation(err)) => { + diagnostics::OperationDiagnostic::native_tls(native_tls) + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) + } Err(err) => Err(err.into()), } } diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 6c6243410db1..902070d3ac64 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -288,7 +288,7 @@ pub(crate) async fn remove( { Ok(result) => result.into_lock(), Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -349,7 +349,7 @@ pub(crate) async fn remove( { Ok(()) => {} Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 16cd4b8f01ed..2eb54f448d3b 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -284,7 +284,8 @@ pub(crate) async fn run( let environment = match result { Ok(resolution) => resolution, Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::with_context("script") + return diagnostics::OperationDiagnostic::native_tls(native_tls) + .with_context("script") .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -415,7 +416,8 @@ pub(crate) async fn run( let environment = match result { Ok(resolution) => resolution, Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::with_context("script") + return diagnostics::OperationDiagnostic::native_tls(native_tls) + .with_context("script") .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -738,7 +740,7 @@ pub(crate) async fn run( { Ok(result) => result, Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -819,7 +821,7 @@ pub(crate) async fn run( { Ok(()) => {} Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -972,7 +974,8 @@ pub(crate) async fn run( let environment = match result { Ok(resolution) => resolution, Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::with_context("`--with`") + return diagnostics::OperationDiagnostic::native_tls(native_tls) + .with_context("`--with`") .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 755a4314c3b7..4d1da92e07ed 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -160,7 +160,7 @@ pub(crate) async fn sync( { Ok(result) => result.into_lock(), Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -236,7 +236,7 @@ pub(crate) async fn sync( { Ok(()) => {} Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 1c612cceabfe..384eada5e634 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -162,7 +162,7 @@ pub(crate) async fn tree( { Ok(result) => result.into_lock(), Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 958dd30bcb50..3d00c10e12af 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -431,7 +431,7 @@ pub(crate) async fn install( { Ok(update) => update.into_environment(), Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } @@ -491,7 +491,7 @@ pub(crate) async fn install( .await .ok() .flatten() else { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())); }; @@ -520,7 +520,7 @@ pub(crate) async fn install( { Ok(resolution) => resolution, Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())); } @@ -563,7 +563,7 @@ pub(crate) async fn install( }) { Ok(environment) => environment, Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::default() + return diagnostics::OperationDiagnostic::native_tls(native_tls) .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) } diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 79a7a30dab3e..36fc154c7c3c 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -135,7 +135,8 @@ pub(crate) async fn run( let (from, environment) = match result { Ok(resolution) => resolution, Err(ProjectError::Operation(err)) => { - return diagnostics::OperationDiagnostic::with_context("tool") + return diagnostics::OperationDiagnostic::native_tls(native_tls) + .with_context("tool") .report(err) .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) }