Improve certificate loading error messages (#18924)

See https://github.com/astral-sh/uv/issues/18890

Adds special-case validation for `SSL_CERT_FILE` and `SSL_CERT_DIR`
where we actually check if webpki will accept the given certificates
and, if not, emit a better error message about why. This means we
perform eager validation of certificates, parsing them more than once
since reqwest will parse them again on client build. Unfortunately,
there's not a straight-forward way to provide our pre-parsed
certificates to reqwest without doing a lot more work. Nor is there a
clear way to retrieve the parsed certificates on error.

We use https://github.com/rusticata/x509-parser for parsing which seems
reputable.

We may want to _drop_ all invalid certificates instead, but that can be
a future decision and this machinery can be reused for warnings.

Ideally webpki would just have better error messages, but that's a
separate project.
This commit is contained in:
Zanie Blue
2026-04-08 14:16:25 -05:00
committed by GitHub
parent 39b83c30e0
commit f6d67d57c1
12 changed files with 337 additions and 45 deletions
Generated
+2
View File
@@ -6215,6 +6215,7 @@ dependencies = [
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"rustls-webpki",
"serde",
"serde_json",
"temp-env",
@@ -6248,6 +6249,7 @@ dependencies = [
"uv-warnings",
"webpki-root-certs",
"wiremock",
"x509-parser",
]
[[package]]
+2
View File
@@ -334,6 +334,8 @@ rcgen = { version = "0.14.5", features = [
], default-features = false }
rustls = { version = "0.23.36", default-features = false }
similar = { version = "2.6.0" }
webpki = { package = "rustls-webpki", version = "0.103.10" }
x509-parser = { version = "0.18.0" }
temp-env = { version = "0.3.6", features = ["async_closure"] }
test-case = { version = "3.3.1" }
test-log = { version = "0.2.16", features = [
+2
View File
@@ -63,6 +63,8 @@ rustc-hash = { workspace = true }
rustls-native-certs = { workspace = true }
rustls-pki-types = { workspace = true }
serde = { workspace = true }
webpki = { workspace = true }
x509-parser = { workspace = true }
serde_json = { workspace = true }
uv-platform = { workspace = true }
thiserror = { workspace = true }
+44 -6
View File
@@ -47,7 +47,7 @@ use uv_warnings::warn_user_once;
use crate::linehaul::LineHaul;
use crate::middleware::OfflineMiddleware;
use crate::tls::{Certificates, read_identity};
use crate::tls::{Certificates, TlsConfigurationError, read_identity};
use crate::{Connectivity, WrappedReqwestError};
pub const DEFAULT_RETRIES: u32 = 3;
@@ -71,6 +71,44 @@ pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
/// timeout on the entire upload.
pub const DEFAULT_READ_TIMEOUT_UPLOAD: Duration = Duration::from_mins(15);
#[derive(Debug)]
pub struct ClientBuildError(ClientBuildErrorKind);
#[derive(Debug, Error)]
enum ClientBuildErrorKind {
#[error(transparent)]
Reqwest(reqwest::Error),
#[error(transparent)]
TlsConfiguration(TlsConfigurationError),
}
impl std::fmt::Display for ClientBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("failed to build HTTP client")
}
}
impl std::error::Error for ClientBuildError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.0 {
ClientBuildErrorKind::Reqwest(error) => Some(error),
ClientBuildErrorKind::TlsConfiguration(error) => Some(error),
}
}
}
impl From<reqwest::Error> for ClientBuildError {
fn from(error: reqwest::Error) -> Self {
Self(ClientBuildErrorKind::Reqwest(error))
}
}
impl From<TlsConfigurationError> for ClientBuildError {
fn from(error: TlsConfigurationError) -> Self {
Self(ClientBuildErrorKind::TlsConfiguration(error))
}
}
/// Selectively skip parts or the entire auth middleware.
#[derive(Debug, Clone, Copy, Default)]
pub enum AuthIntegration {
@@ -380,7 +418,7 @@ impl<'a> BaseClientBuilder<'a> {
retry_policy(self.retries, self.no_retry_delay)
}
pub fn build(&self) -> reqwest::Result<BaseClient> {
pub fn build(&self) -> Result<BaseClient, ClientBuildError> {
if let Some(name) = self.client_name {
debug!(
"Using request connect timeout of {}s and read timeout of {}s for {} client",
@@ -464,7 +502,7 @@ impl<'a> BaseClientBuilder<'a> {
&self,
read_timeout: Duration,
connect_timeout: Duration,
) -> reqwest::Result<(Client, Client)> {
) -> Result<(Client, Client), ClientBuildError> {
// Create user agent.
let mut user_agent_string = format!("uv/{}", version());
@@ -475,7 +513,7 @@ impl<'a> BaseClientBuilder<'a> {
}
// Load custom CA certificates from `SSL_CERT_FILE` and `SSL_CERT_DIR`.
let custom_certs = Certificates::from_env().map(|certs| certs.to_reqwest_certs());
let custom_certs = Certificates::from_env()?.map(|certs| certs.to_reqwest_certs());
// Create a secure client that validates certificates.
let raw_client = self.create_client(
@@ -508,7 +546,7 @@ impl<'a> BaseClientBuilder<'a> {
custom_certs: Option<Vec<Certificate>>,
security: Security,
redirect_policy: RedirectPolicy,
) -> reqwest::Result<Client> {
) -> Result<Client, ClientBuildError> {
// Configure the builder.
let client_builder = ClientBuilder::new()
.http1_title_case_headers()
@@ -574,7 +612,7 @@ impl<'a> BaseClientBuilder<'a> {
client_builder = client_builder.proxy(proxy);
}
client_builder.build()
client_builder.build().map_err(Into::into)
}
fn apply_middleware(&self, client: Client) -> ClientWithMiddleware {
+3 -3
View File
@@ -1,7 +1,7 @@
pub use base_client::{
AuthIntegration, BaseClient, BaseClientBuilder, DEFAULT_CONNECT_TIMEOUT, DEFAULT_MAX_REDIRECTS,
DEFAULT_READ_TIMEOUT, DEFAULT_READ_TIMEOUT_UPLOAD, DEFAULT_RETRIES, ExtraMiddleware,
RedirectClientWithMiddleware, RedirectPolicy, RequestBuilder, RetriableError,
AuthIntegration, BaseClient, BaseClientBuilder, ClientBuildError, DEFAULT_CONNECT_TIMEOUT,
DEFAULT_MAX_REDIRECTS, DEFAULT_READ_TIMEOUT, DEFAULT_READ_TIMEOUT_UPLOAD, DEFAULT_RETRIES,
ExtraMiddleware, RedirectClientWithMiddleware, RedirectPolicy, RequestBuilder, RetriableError,
RetryParsingError, RetryState, UvRetryableStrategy, fetch_with_url_fallback,
retryable_on_request_failure,
};
+2 -2
View File
@@ -37,7 +37,7 @@ use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString;
use uv_torch::TorchStrategy;
use crate::base_client::{BaseClientBuilder, ExtraMiddleware, RedirectPolicy};
use crate::base_client::{BaseClientBuilder, ClientBuildError, ExtraMiddleware, RedirectPolicy};
use crate::cached_client::CacheControl;
use crate::flat_index::FlatIndexEntry;
use crate::html::SimpleDetailHTML;
@@ -163,7 +163,7 @@ impl<'a> RegistryClientBuilder<'a> {
}
}
pub fn build(mut self) -> reqwest::Result<RegistryClient> {
pub fn build(mut self) -> Result<RegistryClient, ClientBuildError> {
self.cache_index_credentials();
let index_urls = self.index_locations.index_urls();
+194 -23
View File
@@ -1,4 +1,5 @@
use std::env;
use std::fmt::{Display, Formatter};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
@@ -7,11 +8,148 @@ use reqwest::{Certificate, Identity};
use rustls_native_certs::{CertificateResult, load_certs_from_paths};
use rustls_pki_types::CertificateDer;
use tracing::debug;
use webpki::{Error as WebPkiError, anchor_from_trusted_cert};
use x509_parser::prelude::{FromDer, X509Certificate};
use uv_fs::Simplified;
use uv_static::EnvVars;
use uv_warnings::warn_user_once;
#[derive(Debug, Clone)]
pub(crate) enum CertificateSource {
SslCertFile(PathBuf),
SslCertDir(PathBuf),
}
impl CertificateSource {
const fn env_var(&self) -> &'static str {
match self {
Self::SslCertFile(_) => EnvVars::SSL_CERT_FILE,
Self::SslCertDir(_) => EnvVars::SSL_CERT_DIR,
}
}
fn path(&self) -> &Path {
match self {
Self::SslCertFile(path) | Self::SslCertDir(path) => path,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DiagnosticCertificate(CertificateDer<'static>);
impl DiagnosticCertificate {
fn parse(&self) -> Option<X509Certificate<'_>> {
let (_, certificate) = X509Certificate::from_der(self.0.as_ref()).ok()?;
Some(certificate)
}
}
#[derive(Debug)]
pub(crate) enum TlsConfigurationError {
UnsupportedCriticalExtension {
source: CertificateSource,
certificate: DiagnosticCertificate,
},
InvalidTrustAnchor {
source: CertificateSource,
certificate: DiagnosticCertificate,
error: WebPkiError,
},
}
impl TlsConfigurationError {
fn from_webpki_error(
source: CertificateSource,
error: WebPkiError,
cert: &CertificateDer<'_>,
) -> Self {
let certificate = DiagnosticCertificate(cert.clone().into_owned());
match error {
WebPkiError::UnsupportedCriticalExtension => Self::UnsupportedCriticalExtension {
source,
certificate,
},
error => Self::InvalidTrustAnchor {
source,
certificate,
error,
},
}
}
}
impl Display for TlsConfigurationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedCriticalExtension {
source,
certificate,
} => {
write!(
f,
"certificate in `{}` (from `{}`) uses an unsupported critical extension",
source.path().simplified_display(),
source.env_var()
)?;
if let Some(certificate) = certificate.parse() {
let subject = certificate.subject();
if subject.iter_attributes().next().is_some() {
write!(f, " on certificate `{subject}`")?;
}
let critical_extensions = certificate
.iter_extensions()
.filter(|extension| extension.critical)
.map(|extension| extension.oid.to_owned())
.collect::<Vec<_>>();
if let [critical_extension] = critical_extensions.as_slice() {
write!(f, "; critical extension: `{critical_extension}`")?;
} else if !critical_extensions.is_empty() {
write!(
f,
"; critical extensions: {}",
critical_extensions
.iter()
.map(|oid| format!("`{oid}`"))
.join(", ")
)?;
}
}
Ok(())
}
Self::InvalidTrustAnchor {
source,
certificate,
..
} => {
write!(
f,
"certificate in `{}` (from `{}`) could not be used as a trust anchor",
source.path().simplified_display(),
source.env_var()
)?;
if let Some(certificate) = certificate.parse() {
let subject = certificate.subject();
if subject.iter_attributes().next().is_some() {
write!(f, " on certificate `{subject}`")?;
}
}
Ok(())
}
}
}
}
impl std::error::Error for TlsConfigurationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::UnsupportedCriticalExtension { .. } => None,
Self::InvalidTrustAnchor { error, .. } => Some(error),
}
}
}
/// A collection of TLS certificates in DER form.
#[derive(Debug, Clone, Default)]
pub(crate) struct Certificates(Vec<CertificateDer<'static>>);
@@ -37,34 +175,40 @@ impl Certificates {
/// Returns `None` if neither variable is set, if the referenced files or directories are
/// missing or inaccessible, or if no valid certificates are found (with a warning in each
/// case). Delegates path loading to [`rustls_native_certs::load_certs_from_paths`].
pub(crate) fn from_env() -> Option<Self> {
pub(crate) fn from_env() -> Result<Option<Self>, TlsConfigurationError> {
let mut certs = Self::default();
let mut has_source = false;
if let Some(ssl_cert_file) = env::var_os(EnvVars::SSL_CERT_FILE)
&& let Some(file_certs) = Self::from_ssl_cert_file(&ssl_cert_file)
&& let Some(file_certs) = Self::from_ssl_cert_file(&ssl_cert_file)?
{
has_source = true;
certs.merge(file_certs);
}
if let Some(ssl_cert_dir) = env::var_os(EnvVars::SSL_CERT_DIR)
&& let Some(dir_certs) = Self::from_ssl_cert_dir(&ssl_cert_dir)
&& let Some(dir_certs) = Self::from_ssl_cert_dir(&ssl_cert_dir)?
{
has_source = true;
certs.merge(dir_certs);
}
if has_source { Some(certs) } else { None }
if has_source {
Ok(Some(certs))
} else {
Ok(None)
}
}
/// Load certificates from the value of `SSL_CERT_FILE`.
///
/// Returns `None` if the value is empty, the path does not refer to an accessible file,
/// or the file contains no valid certificates.
fn from_ssl_cert_file(ssl_cert_file: &std::ffi::OsStr) -> Option<Self> {
fn from_ssl_cert_file(
ssl_cert_file: &std::ffi::OsStr,
) -> Result<Option<Self>, TlsConfigurationError> {
if ssl_cert_file.is_empty() {
return None;
return Ok(None);
}
let file = PathBuf::from(ssl_cert_file);
@@ -83,30 +227,31 @@ impl Certificates {
"Ignoring `SSL_CERT_FILE`. No certificates found in: {}.",
file.simplified_display().cyan()
);
return None;
return Ok(None);
}
Some(certs)
certs.validate_trust_anchors(CertificateSource::SslCertFile(file))?;
Ok(Some(certs))
}
Ok(_) => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path is not a file: {}.",
file.simplified_display().cyan()
);
None
Ok(None)
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path does not exist: {}.",
file.simplified_display().cyan()
);
None
Ok(None)
}
Err(err) => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path is not accessible: {} ({err}).",
file.simplified_display().cyan()
);
None
Ok(None)
}
}
}
@@ -118,9 +263,11 @@ impl Certificates {
///
/// Returns `None` if the value is empty, no listed directories exist, or no valid
/// certificates are found.
fn from_ssl_cert_dir(ssl_cert_dir: &std::ffi::OsStr) -> Option<Self> {
fn from_ssl_cert_dir(
ssl_cert_dir: &std::ffi::OsStr,
) -> Result<Option<Self>, TlsConfigurationError> {
if ssl_cert_dir.is_empty() {
return None;
return Ok(None);
}
let (existing, missing): (Vec<_>, Vec<_>) =
@@ -140,7 +287,7 @@ impl Certificates {
.join(", ")
.cyan()
);
return None;
return Ok(None);
}
if !missing.is_empty() {
@@ -168,7 +315,11 @@ impl Certificates {
dir.simplified_display().cyan()
);
}
certs.merge(Self::from(result));
let dir_certs = Self::from(result);
if !dir_certs.0.is_empty() {
dir_certs.validate_trust_anchors(CertificateSource::SslCertDir(dir.clone()))?;
certs.merge(dir_certs);
}
}
if certs.0.is_empty() {
@@ -180,10 +331,10 @@ impl Certificates {
.join(", ")
.cyan()
);
return None;
return Ok(None);
}
Some(certs)
Ok(Some(certs))
}
/// Load certificates from explicit file and directory paths.
@@ -191,6 +342,26 @@ impl Certificates {
load_certs_from_paths(file, dir)
}
fn validate_trust_anchors(
&self,
source: CertificateSource,
) -> Result<(), TlsConfigurationError> {
for cert in &self.0 {
if let Err(error) = anchor_from_trusted_cert(cert) {
debug!(
"Failed to validate certificate from `{}` ({}): {error}",
source.env_var(),
source.path().simplified_display()
);
return Err(TlsConfigurationError::from_webpki_error(
source, error, cert,
));
}
}
Ok(())
}
/// Remove duplicate certificates, sorting by DER bytes.
fn dedup(&mut self) {
self.0
@@ -272,13 +443,13 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let missing_file = dir.path().join("missing.pem");
let certs = Certificates::from_ssl_cert_file(missing_file.as_os_str());
let certs = Certificates::from_ssl_cert_file(missing_file.as_os_str()).unwrap();
assert!(certs.is_none());
}
#[test]
fn test_from_ssl_cert_file_empty_value_returns_none() {
let certs = Certificates::from_ssl_cert_file(OsString::new().as_os_str());
let certs = Certificates::from_ssl_cert_file(OsString::new().as_os_str()).unwrap();
assert!(certs.is_none());
}
@@ -288,13 +459,13 @@ mod tests {
let cert_path = dir.path().join("empty.pem");
fs_err::write(&cert_path, "not a certificate").unwrap();
let certs = Certificates::from_ssl_cert_file(cert_path.as_os_str());
let certs = Certificates::from_ssl_cert_file(cert_path.as_os_str()).unwrap();
assert!(certs.is_none());
}
#[test]
fn test_from_ssl_cert_dir_empty_value_returns_none() {
let certs = Certificates::from_ssl_cert_dir(OsString::new().as_os_str());
let certs = Certificates::from_ssl_cert_dir(OsString::new().as_os_str()).unwrap();
assert!(certs.is_none());
}
@@ -304,7 +475,7 @@ mod tests {
let missing_dir = dir.path().join("missing-dir");
let cert_dirs = std::env::join_paths([&missing_dir]).unwrap();
let certs = Certificates::from_ssl_cert_dir(cert_dirs.as_os_str());
let certs = Certificates::from_ssl_cert_dir(cert_dirs.as_os_str()).unwrap();
assert!(certs.is_none());
}
@@ -313,7 +484,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let cert_dirs = std::env::join_paths([dir.path()]).unwrap();
let certs = Certificates::from_ssl_cert_dir(cert_dirs.as_os_str());
let certs = Certificates::from_ssl_cert_dir(cert_dirs.as_os_str()).unwrap();
assert!(certs.is_none());
}
+71 -6
View File
@@ -1,9 +1,11 @@
use std::error::Error;
use std::io::Write;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::Result;
use insta::{assert_snapshot, with_settings};
use rcgen::CustomExtension;
use temp_env::async_with_vars;
use tempfile::{NamedTempFile, TempDir};
@@ -12,6 +14,7 @@ use url::Url;
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_client::RegistryClientBuilder;
use uv_fs::Simplified;
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars;
@@ -37,6 +40,13 @@ struct TestCertificate {
client_cert_path: PathBuf,
}
fn temp_dir_filter(path: &Path) -> String {
format!(
r"{}\\?/?",
regex::escape(&path.simplified_display().to_string()).replace(r"\\", r"(\\|/)")
)
}
impl TestCertificate {
/// Generate a fresh CA, server cert, and client cert, persisting the
/// relevant PEM files to a temporary directory.
@@ -59,6 +69,19 @@ impl TestCertificate {
Self::persist(ca, server, &client)
}
/// Generate a fresh certificate set whose CA contains a duplicate
/// `basicConstraints` extension, which webpki rejects as an invalid trust
/// anchor.
fn new_with_duplicate_basic_constraints_ca_extension() -> Result<Self> {
let duplicate_basic_constraints =
CustomExtension::from_oid_content(&[2, 5, 29, 19], vec![0x30, 0x00]);
let (ca, server, client) = generate_self_signed_certs_with_ca_custom_extensions(vec![
duplicate_basic_constraints,
])?;
Self::persist(ca, server, &client)
}
fn persist(ca: SelfSigned, server: SelfSigned, client: &SelfSigned) -> Result<Self> {
let cert_dir = test_cert_dir();
fs_err::create_dir_all(&cert_dir)?;
@@ -461,16 +484,58 @@ async fn test_ssl_cert_file_unsupported_critical_extension_returns_error() -> Re
let test_client = client().ssl_cert_file(&cert.trust_path);
let vars = test_client.ssl_vars();
async_with_vars(vars, async {
let temp_dir_filter = temp_dir_filter(cert._temp_dir.path());
async_with_vars(vars, async move {
let err = BaseClientBuilder::default()
.build()
.expect_err("expected client build to fail");
assert!(err.is_builder(), "expected builder error, got: {err:?}");
assert!(
format!("{err:?}").contains("UnsupportedCriticalExtension"),
"expected error to mention UnsupportedCriticalExtension, got: {err:?}"
);
let source = err.source().expect("expected client build error source");
let display = format!("{err}\nCaused by: {source}");
with_settings!({
filters => vec![(temp_dir_filter.as_str(), "[TMP]/")]
}, {
assert_snapshot!(display, @r#"
failed to build HTTP client
Caused by: certificate in `[TMP]/ca.pem` (from `SSL_CERT_FILE`) uses an unsupported critical extension on certificate `CN=uv-test-ca, O=Astral Software Inc.`; critical extensions: `2.5.29.15`, `2.5.29.19`, `1.2.3.4`
"#);
});
})
.await;
Ok(())
}
/// An invalid trust anchor in `SSL_CERT_FILE` returns a builder error with a
/// generic trust-anchor message.
#[tokio::test]
async fn test_ssl_cert_file_invalid_trust_anchor_returns_error() -> Result<()> {
let cert = TestCertificate::new_with_duplicate_basic_constraints_ca_extension()?;
let test_client = client().ssl_cert_file(&cert.trust_path);
let vars = test_client.ssl_vars();
let temp_dir_filter = temp_dir_filter(cert._temp_dir.path());
async_with_vars(vars, async move {
let err = BaseClientBuilder::default()
.build()
.expect_err("expected client build to fail");
let source = err.source().expect("expected client build error source");
let next_source = source.source().expect("expected trust anchor validation cause");
let display = format!("{err}\nCaused by: {source}\nCaused by: {next_source}");
with_settings!({
filters => vec![(temp_dir_filter.as_str(), "[TMP]/")]
}, {
assert_snapshot!(display, @r#"
failed to build HTTP client
Caused by: certificate in `[TMP]/ca.pem` (from `SSL_CERT_FILE`) could not be used as a trust anchor on certificate `CN=uv-test-ca, O=Astral Software Inc.`
Caused by: ExtensionValueInvalid
"#);
});
})
.await;
+1 -1
View File
@@ -88,7 +88,7 @@ pub enum Error {
Download(#[from] downloads::Error),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
ClientBuild(#[from] uv_client::ClientBuildError),
// TODO(zanieb) We might want to ensure this is always wrapped in another type
#[error(transparent)]
+14 -2
View File
@@ -46,7 +46,7 @@ use unscanny::{Pattern, Scanner};
use url::Url;
#[cfg(feature = "http")]
use uv_client::BaseClient;
use uv_client::{BaseClient, ClientBuildError};
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{NoBinary, NoBuild, PackageNameSpecifier};
use uv_distribution_types::{
@@ -302,7 +302,7 @@ impl RequirementsTxt {
.build()
.map_err(|err| RequirementsTxtFileError {
file: requirements_txt.to_path_buf(),
error: RequirementsTxtParserError::from_reqwest(url, err),
error: RequirementsTxtParserError::ClientBuild(url.clone(), Box::new(err)),
})?;
let content = read_url_to_string(&requirements_txt, client)
.await
@@ -1209,6 +1209,8 @@ pub enum RequirementsTxtParserError {
#[cfg(feature = "http")]
Reqwest(DisplaySafeUrl, reqwest_middleware::Error),
#[cfg(feature = "http")]
ClientBuild(DisplaySafeUrl, Box<ClientBuildError>),
#[cfg(feature = "http")]
InvalidUrl(String, DisplaySafeUrlError),
}
@@ -1280,6 +1282,10 @@ impl Display for RequirementsTxtParserError {
write!(f, "Error while accessing remote requirements file: `{url}`")
}
#[cfg(feature = "http")]
Self::ClientBuild(url, _err) => {
write!(f, "Error while accessing remote requirements file: `{url}`")
}
#[cfg(feature = "http")]
Self::InvalidUrl(url, err) => {
match err {
DisplaySafeUrlError::Url(err) => write!(f, "Not a valid URL, {err}: `{url}`"),
@@ -1318,6 +1324,8 @@ impl std::error::Error for RequirementsTxtParserError {
#[cfg(feature = "http")]
Self::Reqwest(_, err) => err.source(),
#[cfg(feature = "http")]
Self::ClientBuild(_, err) => Some(err.as_ref()),
#[cfg(feature = "http")]
Self::InvalidUrl(_, err) => err.source(),
}
}
@@ -1448,6 +1456,10 @@ impl Display for RequirementsTxtFileError {
write!(f, "Error while accessing remote requirements file: `{url}`")
}
#[cfg(feature = "http")]
RequirementsTxtParserError::ClientBuild(url, _err) => {
write!(f, "Error while accessing remote requirements file: `{url}`")
}
#[cfg(feature = "http")]
RequirementsTxtParserError::InvalidUrl(url, err) => match err {
DisplaySafeUrlError::Url(err) => write!(f, "Not a valid URL, {err}: `{url}`"),
DisplaySafeUrlError::AmbiguousAuthority(_) => {
+1 -1
View File
@@ -61,7 +61,7 @@ enum Error {
#[error(transparent)]
FlatIndex(#[from] uv_client::FlatIndexError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
ClientBuild(#[from] uv_client::ClientBuildError),
#[error(transparent)]
BuildPlan(anyhow::Error),
#[error(transparent)]
+1 -1
View File
@@ -279,7 +279,7 @@ pub(crate) enum ProjectError {
Client(#[from] uv_client::Error),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
ClientBuild(#[from] uv_client::ClientBuildError),
#[error(transparent)]
Python(#[from] uv_python::Error),