use backon::ExponentialBuilder;
#[must_use]
pub fn is_retryable_http(status: u16) -> bool {
matches!(status, 429 | 500 | 502 | 503 | 504)
}
#[must_use]
pub fn is_retryable_octocrab(e: &octocrab::Error) -> bool {
match e {
octocrab::Error::GitHub { source, .. } => {
matches!(
source.status_code.as_u16(),
429 | 500 | 502 | 503 | 504 | 403
)
}
octocrab::Error::Service { .. } | octocrab::Error::Hyper { .. } => true,
_ => false,
}
}
#[must_use]
pub fn is_retryable_anyhow(e: &anyhow::Error) -> bool {
if let Some(oct_err) = e.downcast_ref::<octocrab::Error>() {
return is_retryable_octocrab(oct_err);
}
if let Some(req_err) = e.downcast_ref::<reqwest::Error>() {
if req_err.is_timeout() || req_err.is_connect() {
return true;
}
if let Some(status) = req_err.status() {
return is_retryable_http(status.as_u16());
}
}
if let Some(aptu_err) = e.downcast_ref::<crate::error::AptuError>() {
return matches!(
aptu_err,
crate::error::AptuError::RateLimited { .. }
| crate::error::AptuError::TruncatedResponse { .. }
);
}
false
}
#[must_use]
pub fn retry_backoff() -> ExponentialBuilder {
ExponentialBuilder::default()
.with_factor(2.0)
.with_min_delay(std::time::Duration::from_secs(1))
.with_max_times(3)
.with_jitter()
}
const MAX_RETRY_AFTER_SECS: u64 = 120;
#[must_use]
pub fn extract_retry_after(e: &anyhow::Error) -> Option<std::time::Duration> {
if let Some(crate::error::AptuError::RateLimited { retry_after, .. }) =
e.downcast_ref::<crate::error::AptuError>()
&& *retry_after > 0
{
let capped = (*retry_after).min(MAX_RETRY_AFTER_SECS);
return Some(std::time::Duration::from_secs(capped));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_retryable_http_429() {
assert!(is_retryable_http(429));
}
#[test]
fn test_is_retryable_http_500() {
assert!(is_retryable_http(500));
}
#[test]
fn test_is_retryable_http_502() {
assert!(is_retryable_http(502));
}
#[test]
fn test_is_retryable_http_503() {
assert!(is_retryable_http(503));
}
#[test]
fn test_is_retryable_http_504() {
assert!(is_retryable_http(504));
}
#[test]
fn test_is_retryable_http_non_retryable() {
assert!(!is_retryable_http(400));
assert!(!is_retryable_http(401));
assert!(!is_retryable_http(403));
assert!(!is_retryable_http(404));
assert!(!is_retryable_http(200));
assert!(!is_retryable_http(201));
}
#[test]
fn test_retry_backoff_configuration() {
let backoff = retry_backoff();
let _: ExponentialBuilder = backoff;
}
#[test]
fn test_is_retryable_anyhow_with_non_retryable() {
let err = anyhow::anyhow!("some other error");
assert!(!is_retryable_anyhow(&err));
}
#[test]
fn test_is_retryable_http_retryable_codes() {
assert!(is_retryable_http(429));
assert!(is_retryable_http(500));
assert!(is_retryable_http(502));
assert!(is_retryable_http(503));
assert!(is_retryable_http(504));
}
#[test]
fn test_is_retryable_http_non_retryable_codes() {
assert!(!is_retryable_http(400));
assert!(!is_retryable_http(401));
assert!(!is_retryable_http(403));
assert!(!is_retryable_http(404));
assert!(!is_retryable_http(200));
assert!(!is_retryable_http(201));
}
#[test]
fn test_is_retryable_anyhow_with_truncated_response() {
let err = anyhow::anyhow!(crate::error::AptuError::TruncatedResponse {
provider: "OpenRouter".to_string(),
});
assert!(is_retryable_anyhow(&err));
}
#[test]
fn test_is_retryable_anyhow_with_rate_limited() {
let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
provider: "OpenRouter".to_string(),
retry_after: 60,
});
assert!(is_retryable_anyhow(&err));
}
#[test]
fn test_extract_retry_after_with_valid_value() {
let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
provider: "OpenRouter".to_string(),
retry_after: 60,
});
let duration = extract_retry_after(&err);
assert_eq!(duration, Some(std::time::Duration::from_secs(60)));
}
#[test]
fn test_extract_retry_after_with_zero_value() {
let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
provider: "OpenRouter".to_string(),
retry_after: 0,
});
let duration = extract_retry_after(&err);
assert_eq!(duration, None);
}
#[test]
fn test_extract_retry_after_with_capped_value() {
let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
provider: "OpenRouter".to_string(),
retry_after: 300,
});
let duration = extract_retry_after(&err);
assert_eq!(duration, Some(std::time::Duration::from_secs(120)));
}
#[test]
fn test_extract_retry_after_with_non_rate_limited_error() {
let err = anyhow::anyhow!("some other error");
let duration = extract_retry_after(&err);
assert_eq!(duration, None);
}
#[test]
fn test_extract_retry_after_with_truncated_response() {
let err = anyhow::anyhow!(crate::error::AptuError::TruncatedResponse {
provider: "OpenRouter".to_string(),
});
let duration = extract_retry_after(&err);
assert_eq!(duration, None);
}
}