Skip to content

Commit

Permalink
Recommend --native-tls on SSL errors
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jan 14, 2025
1 parent d8b5e7e commit 9378550
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 38 deletions.
46 changes: 37 additions & 9 deletions crates/uv-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>() {
Expand All @@ -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;
}
Expand All @@ -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 {
Expand Down
60 changes: 56 additions & 4 deletions crates/uv/src/commands/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,37 @@ static SUGGESTIONS: LazyLock<FxHashMap<PackageName, PackageName>> = LazyLock::ne
pub(crate) struct OperationDiagnostic {
/// The hint to display to the user upon resolution failure.
pub(crate) hint: Option<String>,
/// 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
}
}

Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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 native TLS support via 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.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/pip/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/pip/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down
8 changes: 5 additions & 3 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down
13 changes: 8 additions & 5 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down
8 changes: 4 additions & 4 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down Expand Up @@ -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()));
};
Expand Down Expand Up @@ -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()));
}
Expand Down Expand Up @@ -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()))
}
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down

0 comments on commit 9378550

Please sign in to comment.