use std::io;
use std::time::Duration;
use ureq::config::IpFamily;
pub fn get(url: &str, timeout: Duration, user_agent: Option<&str>) -> Result<String, ureq::Error> {
with_family_fallback(&mut |family| {
let mut config = ureq::Agent::config_builder()
.timeout_global(Some(timeout))
.ip_family(family);
if let Some(ua) = user_agent {
config = config.user_agent(ua);
}
let agent = config.build().new_agent();
agent.get(url).call()?.body_mut().read_to_string()
})
}
fn with_family_fallback(
attempt: &mut dyn FnMut(IpFamily) -> Result<String, ureq::Error>,
) -> Result<String, ureq::Error> {
let original = match attempt(IpFamily::Any) {
Err(e) if is_unroutable(&e) => e,
other => return other,
};
for family in [IpFamily::Ipv4Only, IpFamily::Ipv6Only] {
match attempt(family) {
Err(e) if is_unroutable(&e) || matches!(e, ureq::Error::HostNotFound) => {}
other => return other,
}
}
Err(original)
}
fn is_unroutable(error: &ureq::Error) -> bool {
let ureq::Error::Io(io) = error else {
return false;
};
matches!(
io.kind(),
io::ErrorKind::HostUnreachable
| io::ErrorKind::NetworkUnreachable
| io::ErrorKind::AddrNotAvailable
)
}
#[cfg(test)]
mod tests {
use super::*;
fn unroutable(kind: io::ErrorKind) -> ureq::Error {
ureq::Error::Io(io::Error::from(kind))
}
#[test]
fn an_unroutable_connect_is_retried_one_family_at_a_time() {
let mut tried = Vec::new();
let result = with_family_fallback(&mut |family| {
tried.push(family);
match family {
IpFamily::Ipv4Only => Ok("body".to_string()),
_ => Err(unroutable(io::ErrorKind::HostUnreachable)),
}
});
assert_eq!(result.unwrap(), "body");
assert_eq!(tried, vec![IpFamily::Any, IpFamily::Ipv4Only]);
}
#[test]
fn a_refused_connect_is_not_retried() {
let mut attempts = 0;
let result = with_family_fallback(&mut |_| {
attempts += 1;
Err(unroutable(io::ErrorKind::ConnectionRefused))
});
assert!(result.is_err());
assert_eq!(attempts, 1);
}
#[test]
fn a_status_error_from_a_retry_is_the_answer() {
let result = with_family_fallback(&mut |family| match family {
IpFamily::Any => Err(unroutable(io::ErrorKind::HostUnreachable)),
_ => Err(ureq::Error::StatusCode(429)),
});
assert!(matches!(result, Err(ureq::Error::StatusCode(429))));
}
#[test]
fn a_failed_fallback_reports_the_original_error() {
let result = with_family_fallback(&mut |family| match family {
IpFamily::Any => Err(unroutable(io::ErrorKind::HostUnreachable)),
IpFamily::Ipv4Only => Err(ureq::Error::HostNotFound),
IpFamily::Ipv6Only => Err(unroutable(io::ErrorKind::NetworkUnreachable)),
});
match result {
Err(ureq::Error::Io(e)) => assert_eq!(e.kind(), io::ErrorKind::HostUnreachable),
other => panic!("expected the original io error back, got {other:?}"),
}
}
#[test]
fn only_routing_level_failures_trigger_the_fallback() {
for kind in [
io::ErrorKind::HostUnreachable,
io::ErrorKind::NetworkUnreachable,
io::ErrorKind::AddrNotAvailable,
] {
assert!(is_unroutable(&unroutable(kind)), "{kind:?} should trigger");
}
for kind in [io::ErrorKind::ConnectionRefused, io::ErrorKind::TimedOut] {
assert!(!is_unroutable(&unroutable(kind)), "{kind:?} should not");
}
assert!(!is_unroutable(&ureq::Error::StatusCode(500)));
}
}