Skip to content

Commit e6938fe

Browse files
fix(api): Retry API requests on DNS resolution failure
When DNS resolution fails (CURLE_COULDNT_RESOLVE_HOST), the CLI now retries the request using the existing exponential backoff retry mechanism. This addresses intermittent DNS failures that were causing ~5-10% of builds to fail. Fixes #2763 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 922c856 commit e6938fe

File tree

2 files changed

+44
-6
lines changed

2 files changed

+44
-6
lines changed

src/api/errors/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,20 @@ impl RetryError {
2828
self.body
2929
}
3030
}
31+
32+
#[derive(Debug, thiserror::Error)]
33+
#[error("request failed with retryable curl error: {source}")]
34+
pub(super) struct RetryableCurlError {
35+
#[source]
36+
source: curl::Error,
37+
}
38+
39+
impl RetryableCurlError {
40+
pub fn new(source: curl::Error) -> Self {
41+
Self { source }
42+
}
43+
44+
pub fn into_source(self) -> curl::Error {
45+
self.source
46+
}
47+
}

src/api/mod.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod serialization;
1515
use std::borrow::Cow;
1616
use std::cell::RefCell;
1717
use std::collections::HashMap;
18+
use std::error::Error as _;
1819
#[cfg(any(target_os = "macos", not(feature = "managed")))]
1920
use std::fs::File;
2021
use std::io::{self, Read as _, Write};
@@ -41,7 +42,7 @@ use symbolic::common::DebugId;
4142
use symbolic::debuginfo::ObjectKind;
4243
use uuid::Uuid;
4344

44-
use crate::api::errors::{ProjectRenamedError, RetryError};
45+
use crate::api::errors::{ProjectRenamedError, RetryError, RetryableCurlError};
4546
use crate::config::{Auth, Config};
4647
use crate::constants::{ARCH, EXT, PLATFORM, RELEASE_REGISTRY_LATEST_URL, VERSION};
4748
use crate::utils::http::{self, is_absolute_url};
@@ -1313,7 +1314,20 @@ impl ApiRequest {
13131314
debug!("retry number {retry_number}, max retries: {max_retries}");
13141315
*retry_number += 1;
13151316

1316-
let mut rv = self.send_into(&mut out)?;
1317+
let result = self.send_into(&mut out);
1318+
1319+
// Check for retriable curl errors (DNS resolution failure)
1320+
if let Some(curl_err) = result
1321+
.as_ref()
1322+
.err()
1323+
.and_then(|e| e.source())
1324+
.and_then(|s| s.downcast_ref::<curl::Error>())
1325+
.filter(|e| e.is_couldnt_resolve_host())
1326+
{
1327+
anyhow::bail!(RetryableCurlError::new(curl_err.clone()));
1328+
}
1329+
1330+
let mut rv = result?;
13171331
rv.body = Some(out);
13181332

13191333
if RETRY_STATUS_CODES.contains(&rv.status) {
@@ -1326,7 +1340,7 @@ impl ApiRequest {
13261340
send_req
13271341
.retry(backoff)
13281342
.sleep(thread::sleep)
1329-
.when(|e| e.is::<RetryError>())
1343+
.when(|e| e.is::<RetryError>() || e.is::<RetryableCurlError>())
13301344
.notify(|e, dur| {
13311345
debug!(
13321346
"retry number {} failed due to {e:#}, retrying again in {} ms",
@@ -1335,9 +1349,16 @@ impl ApiRequest {
13351349
);
13361350
})
13371351
.call()
1338-
.or_else(|err| match err.downcast::<RetryError>() {
1339-
Ok(err) => Ok(err.into_body()),
1340-
Err(err) => Err(ApiError::with_source(ApiErrorKind::RequestFailed, err)),
1352+
.or_else(|err| {
1353+
err.downcast::<RetryError>()
1354+
.map(RetryError::into_body)
1355+
.map_err(|err| {
1356+
err.downcast::<RetryableCurlError>()
1357+
.map(|e| ApiError::from(e.into_source()))
1358+
.unwrap_or_else(|e| {
1359+
ApiError::with_source(ApiErrorKind::RequestFailed, e)
1360+
})
1361+
})
13411362
})
13421363
}
13431364
}

0 commit comments

Comments
 (0)