Skip to content

Commit e5f8b7e

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 <[email protected]>
1 parent 922c856 commit e5f8b7e

File tree

2 files changed

+38
-4
lines changed

2 files changed

+38
-4
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: 21 additions & 4 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 StdError;
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 Err(ref api_err) = result {
1321+
if let Some(source) = api_err.source() {
1322+
if let Some(curl_err) = source.downcast_ref::<curl::Error>() {
1323+
if curl_err.is_couldnt_resolve_host() {
1324+
anyhow::bail!(RetryableCurlError::new(curl_err.clone()));
1325+
}
1326+
}
1327+
}
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",
@@ -1337,7 +1351,10 @@ impl ApiRequest {
13371351
.call()
13381352
.or_else(|err| match err.downcast::<RetryError>() {
13391353
Ok(err) => Ok(err.into_body()),
1340-
Err(err) => Err(ApiError::with_source(ApiErrorKind::RequestFailed, err)),
1354+
Err(err) => match err.downcast::<RetryableCurlError>() {
1355+
Ok(curl_err) => Err(ApiError::from(curl_err.into_source())),
1356+
Err(err) => Err(ApiError::with_source(ApiErrorKind::RequestFailed, err)),
1357+
},
13411358
})
13421359
}
13431360
}

0 commit comments

Comments
 (0)