Always attach URL to network errors (#8444)

This commit is contained in:
konsti 2024-10-25 11:10:18 +02:00 committed by GitHub
parent 58b5fd4aff
commit 3eda248ef5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 130 additions and 83 deletions

View file

@ -468,9 +468,9 @@ impl CachedClient {
.execute(req)
.instrument(info_span!("revalidation_request", url = url.as_str()))
.await
.map_err(ErrorKind::from)?
.map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))?
.error_for_status()
.map_err(ErrorKind::from)?;
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
match cached
.cache_policy
.after_response(new_cache_policy_builder, &response)
@ -500,16 +500,17 @@ impl CachedClient {
&self,
req: Request,
) -> Result<(Response, Option<Box<CachePolicy>>), Error> {
trace!("Sending fresh {} request for {}", req.method(), req.url());
let url = req.url().clone();
trace!("Sending fresh {} request for {}", req.method(), url);
let cache_policy_builder = CachePolicyBuilder::new(&req);
let response = self
.0
.for_host(req.url())
.for_host(&url)
.execute(req)
.await
.map_err(ErrorKind::from)?
.map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))?
.error_for_status()
.map_err(ErrorKind::from)?;
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
let cache_policy = cache_policy_builder.build(&response);
let cache_policy = if cache_policy.to_archived().is_storable() {
Some(Box::new(cache_policy))

View file

@ -73,7 +73,7 @@ impl Error {
// The server returned a "Method Not Allowed" error, indicating it doesn't support
// HEAD requests, so we can't check for range requests.
ErrorKind::WrappedReqwestError(err) => {
ErrorKind::WrappedReqwestError(_url, err) => {
if let Some(status) = err.status() {
// If the server doesn't support HEAD requests, we can't check for range
// requests.
@ -180,8 +180,8 @@ pub enum ErrorKind {
MetadataNotFound(WheelFilename, String),
/// An error that happened while making a request or in a reqwest middleware.
#[error(transparent)]
WrappedReqwestError(#[from] WrappedReqwestError),
#[error("Failed to fetch: `{0}`")]
WrappedReqwestError(Url, #[source] WrappedReqwestError),
#[error("Received some unexpected JSON from {url}")]
BadJson { source: serde_json::Error, url: Url },
@ -208,7 +208,7 @@ pub enum ErrorKind {
CacheWrite(#[source] std::io::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
Io(std::io::Error),
#[error("Cache deserialization failed")]
Decode(#[source] rmp_serde::decode::Error),
@ -235,21 +235,19 @@ pub enum ErrorKind {
Offline(String),
}
impl From<reqwest::Error> for ErrorKind {
fn from(error: reqwest::Error) -> Self {
Self::WrappedReqwestError(WrappedReqwestError::from(error))
impl ErrorKind {
pub(crate) fn from_reqwest(url: Url, error: reqwest::Error) -> Self {
Self::WrappedReqwestError(url, WrappedReqwestError::from(error))
}
}
impl From<reqwest_middleware::Error> for ErrorKind {
fn from(err: reqwest_middleware::Error) -> Self {
pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self {
if let reqwest_middleware::Error::Middleware(ref underlying) = err {
if let Some(err) = underlying.downcast_ref::<OfflineError>() {
return Self::Offline(err.url().to_string());
}
}
Self::WrappedReqwestError(WrappedReqwestError(err))
Self::WrappedReqwestError(url, WrappedReqwestError(err))
}
}

View file

@ -160,14 +160,17 @@ impl<'a> FlatIndexClient<'a> {
.header("Accept-Encoding", "gzip")
.header("Accept", "text/html")
.build()
.map_err(ErrorKind::from)?;
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
let parse_simple_response = |response: Response| {
async {
// Use the response URL, rather than the request URL, as the base for relative URLs.
// This ensures that we handle redirects and other URL transformations correctly.
let url = response.url().clone();
let text = response.text().await.map_err(ErrorKind::from)?;
let text = response
.text()
.await
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url)
.map_err(|err| Error::from_html_err(err, url.clone()))?;

View file

@ -232,7 +232,7 @@ impl RegistryClient {
}
Err(err) => match err.into_kind() {
// The package could not be found in the remote index.
ErrorKind::WrappedReqwestError(err) => match err.status() {
ErrorKind::WrappedReqwestError(url, err) => match err.status() {
Some(StatusCode::NOT_FOUND) => {}
Some(StatusCode::UNAUTHORIZED) => {
capabilities.set_unauthorized(index.clone());
@ -240,7 +240,7 @@ impl RegistryClient {
Some(StatusCode::FORBIDDEN) => {
capabilities.set_forbidden(index.clone());
}
_ => return Err(ErrorKind::from(err).into()),
_ => return Err(ErrorKind::WrappedReqwestError(url, err).into()),
},
// The package is unavailable due to a lack of connectivity.
@ -323,7 +323,7 @@ impl RegistryClient {
.header("Accept-Encoding", "gzip")
.header("Accept", MediaType::accepts())
.build()
.map_err(ErrorKind::from)?;
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
let parse_simple_response = |response: Response| {
async {
// Use the response URL, rather than the request URL, as the base for relative URLs.
@ -347,14 +347,20 @@ impl RegistryClient {
let unarchived = match media_type {
MediaType::Json => {
let bytes = response.bytes().await.map_err(ErrorKind::from)?;
let bytes = response
.bytes()
.await
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
let data: SimpleJson = serde_json::from_slice(bytes.as_ref())
.map_err(|err| Error::from_json_err(err, url.clone()))?;
SimpleMetadata::from_files(data.files, package_name, &url)
}
MediaType::Html => {
let text = response.text().await.map_err(ErrorKind::from)?;
let text = response
.text()
.await
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
SimpleMetadata::from_html(&text, package_name, &url)?
}
};
@ -547,7 +553,10 @@ impl RegistryClient {
};
let response_callback = |response: Response| async {
let bytes = response.bytes().await.map_err(ErrorKind::from)?;
let bytes = response
.bytes()
.await
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
info_span!("parse_metadata21")
.in_scope(|| ResolutionMetadata::parse_metadata(bytes.as_ref()))
@ -563,7 +572,7 @@ impl RegistryClient {
.uncached_client(&url)
.get(url.clone())
.build()
.map_err(ErrorKind::from)?;
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
Ok(self
.cached_client()
.get_serde(req, &cache_entry, cache_control, response_callback)
@ -616,7 +625,7 @@ impl RegistryClient {
http::HeaderValue::from_static("identity"),
)
.build()
.map_err(ErrorKind::from)?;
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
// Copy authorization headers from the HEAD request to subsequent requests
let mut headers = HeaderMap::default();
@ -694,7 +703,7 @@ impl RegistryClient {
reqwest::header::HeaderValue::from_static("identity"),
)
.build()
.map_err(ErrorKind::from)?;
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
// Stream the file, searching for the METADATA.
let read_metadata_stream = |response: Response| {

View file

@ -7,12 +7,15 @@ pub(crate) enum CertificateError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
Reqwest(reqwest::Error),
}
/// Return the `Identity` from the provided file.
pub(crate) fn read_identity(ssl_client_cert: &OsStr) -> Result<Identity, CertificateError> {
let mut buf = Vec::new();
fs_err::File::open(ssl_client_cert)?.read_to_end(&mut buf)?;
Ok(Identity::from_pem(&buf)?)
Identity::from_pem(&buf).map_err(|tls_err| {
debug_assert!(tls_err.is_builder(), "must be a rustls::Error internally");
CertificateError::Reqwest(tls_err)
})
}

View file

@ -87,7 +87,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
io::Error::new(
io::ErrorKind::TimedOut,
format!(
"Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).", self.client.unmanaged.timeout().as_secs()
"Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).",
self.client.unmanaged.timeout().as_secs()
),
)
} else {

View file

@ -79,7 +79,7 @@ pub enum PublishPrepareError {
#[derive(Error, Debug)]
pub enum PublishSendError {
#[error("Failed to send POST request")]
ReqwestMiddleware(#[from] reqwest_middleware::Error),
ReqwestMiddleware(#[source] reqwest_middleware::Error),
#[error("Upload failed with status {0}")]
StatusNoBody(StatusCode, #[source] reqwest::Error),
#[error("Upload failed with status code {0}. Server says: {1}")]
@ -336,7 +336,11 @@ pub async fn upload(
}
let response = result.map_err(|err| {
PublishError::PublishSend(file.to_path_buf(), registry.clone(), err.into())
PublishError::PublishSend(
file.to_path_buf(),
registry.clone(),
PublishSendError::ReqwestMiddleware(err),
)
})?;
return handle_response(registry, response)

View file

@ -17,10 +17,10 @@ pub enum TrustedPublishingError {
Var(#[from] VarError),
#[error(transparent)]
Url(#[from] url::ParseError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
ReqwestMiddleware(#[from] reqwest_middleware::Error),
#[error("Failed to fetch: `{0}`")]
Reqwest(Url, #[source] reqwest::Error),
#[error("Failed to fetch: `{0}`")]
ReqwestMiddleware(Url, #[source] reqwest_middleware::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::error::Error),
#[error(
@ -105,8 +105,17 @@ async fn get_audience(
// (RFC 3986).
let audience_url = Url::parse(&format!("https://{}/_/oidc/audience", registry.authority()))?;
debug!("Querying the trusted publishing audience from {audience_url}");
let response = client.get(audience_url).send().await?;
let audience = response.error_for_status()?.json::<Audience>().await?;
let response = client
.get(audience_url.clone())
.send()
.await
.map_err(|err| TrustedPublishingError::ReqwestMiddleware(audience_url.clone(), err))?;
let audience = response
.error_for_status()
.map_err(|err| TrustedPublishingError::Reqwest(audience_url.clone(), err))?
.json::<Audience>()
.await
.map_err(|err| TrustedPublishingError::Reqwest(audience_url.clone(), err))?;
trace!("The audience is `{}`", &audience.audience);
Ok(audience.audience)
}
@ -123,11 +132,17 @@ async fn get_oidc_token(
debug!("Querying the trusted publishing OIDC token from {oidc_token_url}");
let authorization = format!("bearer {oidc_token_request_token}");
let response = client
.get(oidc_token_url)
.get(oidc_token_url.clone())
.header(header::AUTHORIZATION, authorization)
.send()
.await?;
let oidc_token: OidcToken = response.error_for_status()?.json().await?;
.await
.map_err(|err| TrustedPublishingError::ReqwestMiddleware(oidc_token_url.clone(), err))?;
let oidc_token: OidcToken = response
.error_for_status()
.map_err(|err| TrustedPublishingError::Reqwest(oidc_token_url.clone(), err))?
.json()
.await
.map_err(|err| TrustedPublishingError::Reqwest(oidc_token_url.clone(), err))?;
Ok(oidc_token.value)
}
@ -145,14 +160,18 @@ async fn get_publish_token(
token: oidc_token.to_string(),
};
let response = client
.post(mint_token_url)
.post(mint_token_url.clone())
.body(serde_json::to_vec(&mint_token_payload)?)
.send()
.await?;
.await
.map_err(|err| TrustedPublishingError::ReqwestMiddleware(mint_token_url.clone(), err))?;
// reqwest's implementation of `.json()` also goes through `.bytes()`
let status = response.status();
let body = response.bytes().await?;
let body = response
.bytes()
.await
.map_err(|err| TrustedPublishingError::Reqwest(mint_token_url.clone(), err))?;
if status.is_success() {
let publish_token: PublishToken = serde_json::from_slice(&body)?;

View file

@ -39,10 +39,10 @@ pub enum Error {
InvalidPythonVersion(String),
#[error("Invalid request key (too many parts): {0}")]
TooManyParts(String),
#[error(transparent)]
NetworkError(#[from] WrappedReqwestError),
#[error(transparent)]
NetworkMiddlewareError(#[from] anyhow::Error),
#[error("Failed to download {0}")]
NetworkError(Url, #[source] WrappedReqwestError),
#[error("Failed to download {0}")]
NetworkMiddlewareError(Url, #[source] anyhow::Error),
#[error("Failed to extract archive: {0}")]
ExtractError(String, #[source] uv_extract::Error),
#[error("Failed to hash installation")]
@ -619,18 +619,18 @@ impl ManagedPythonDownload {
}
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::NetworkError(WrappedReqwestError::from(error))
impl Error {
pub(crate) fn from_reqwest(url: Url, err: reqwest::Error) -> Self {
Self::NetworkError(url, WrappedReqwestError::from(err))
}
}
impl From<reqwest_middleware::Error> for Error {
fn from(error: reqwest_middleware::Error) -> Self {
match error {
reqwest_middleware::Error::Middleware(error) => Self::NetworkMiddlewareError(error),
pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self {
match err {
reqwest_middleware::Error::Middleware(error) => {
Self::NetworkMiddlewareError(url, error)
}
reqwest_middleware::Error::Reqwest(error) => {
Self::NetworkError(WrappedReqwestError::from(error))
Self::NetworkError(url, WrappedReqwestError::from(error))
}
}
}
@ -701,10 +701,17 @@ async fn read_url(
Ok((Either::Left(reader), Some(size)))
} else {
let response = client.for_host(url).get(url.clone()).send().await?;
let response = client
.for_host(url)
.get(url.clone())
.send()
.await
.map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?;
// Ensure the request was successful.
response.error_for_status_ref()?;
response
.error_for_status_ref()
.map_err(|err| Error::from_reqwest(url.clone(), err))?;
let size = response.content_length();
let stream = response

View file

@ -800,14 +800,19 @@ async fn read_url_to_string(
let url = Url::from_str(path_utf8)
.map_err(|err| RequirementsTxtParserError::InvalidUrl(path_utf8.to_string(), err))?;
Ok(client
let response = client
.for_host(&url)
.get(url)
.get(url.clone())
.send()
.await?
.error_for_status()?
.await
.map_err(|err| RequirementsTxtParserError::from_reqwest_middleware(url.clone(), err))?;
let text = response
.error_for_status()
.map_err(|err| RequirementsTxtParserError::from_reqwest(url.clone(), err))?
.text()
.await?)
.await
.map_err(|err| RequirementsTxtParserError::from_reqwest(url.clone(), err))?;
Ok(text)
}
/// Error parsing requirements.txt, wrapper with filename
@ -891,7 +896,7 @@ pub enum RequirementsTxtParserError {
url: PathBuf,
},
#[cfg(feature = "http")]
Reqwest(reqwest_middleware::Error),
Reqwest(Url, reqwest_middleware::Error),
#[cfg(feature = "http")]
InvalidUrl(String, url::ParseError),
}
@ -957,8 +962,8 @@ impl Display for RequirementsTxtParserError {
)
}
#[cfg(feature = "http")]
Self::Reqwest(err) => {
write!(f, "Error while accessing remote requirements file {err}")
Self::Reqwest(url, _err) => {
write!(f, "Error while accessing remote requirements file: `{url}`")
}
#[cfg(feature = "http")]
Self::InvalidUrl(url, err) => {
@ -989,7 +994,7 @@ impl std::error::Error for RequirementsTxtParserError {
Self::Parser { .. } => None,
Self::NonUnicodeUrl { .. } => None,
#[cfg(feature = "http")]
Self::Reqwest(err) => err.source(),
Self::Reqwest(_, err) => err.source(),
#[cfg(feature = "http")]
Self::InvalidUrl(_, err) => err.source(),
}
@ -1117,12 +1122,8 @@ impl Display for RequirementsTxtFileError {
)
}
#[cfg(feature = "http")]
RequirementsTxtParserError::Reqwest(err) => {
write!(
f,
"Error while accessing remote requirements file {}: {err}",
self.file.user_display(),
)
RequirementsTxtParserError::Reqwest(url, _err) => {
write!(f, "Error while accessing remote requirements file: `{url}`")
}
#[cfg(feature = "http")]
RequirementsTxtParserError::InvalidUrl(url, err) => {
@ -1145,16 +1146,13 @@ impl From<io::Error> for RequirementsTxtParserError {
}
#[cfg(feature = "http")]
impl From<reqwest::Error> for RequirementsTxtParserError {
fn from(err: reqwest::Error) -> Self {
Self::Reqwest(reqwest_middleware::Error::Reqwest(err))
impl RequirementsTxtParserError {
fn from_reqwest(url: Url, err: reqwest::Error) -> Self {
Self::Reqwest(url, reqwest_middleware::Error::Reqwest(err))
}
}
#[cfg(feature = "http")]
impl From<reqwest_middleware::Error> for RequirementsTxtParserError {
fn from(err: reqwest_middleware::Error) -> Self {
Self::Reqwest(err)
fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self {
Self::Reqwest(url, err)
}
}

View file

@ -6304,6 +6304,7 @@ fn lock_redact_https() -> Result<()> {
----- stderr -----
error: Failed to prepare distributions
Caused by: Failed to download `iniconfig==2.0.0`
Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
"###);
@ -6316,6 +6317,7 @@ fn lock_redact_https() -> Result<()> {
----- stderr -----
error: Failed to prepare distributions
Caused by: Failed to download `iniconfig==2.0.0`
Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
"###);
@ -6355,6 +6357,7 @@ fn lock_redact_https() -> Result<()> {
----- stderr -----
error: Failed to prepare distributions
Caused by: Failed to download `iniconfig==2.0.0`
Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
"###);

View file

@ -9365,6 +9365,7 @@ fn not_found_direct_url() -> Result<()> {
----- stderr -----
error: Failed to download: `iniconfig @ https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl`
Caused by: Failed to fetch: `https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl`
Caused by: HTTP status client error (404 Not Found) for url (https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl)
"###
);