use bytes::Bytes;
use http_body_util::Full;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
async fn spawn_flaky_server(fail_count: usize) -> (u16, Arc<AtomicUsize>) {
use hyper::server::conn::http1;
use hyper::service::service_fn;
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let port = listener.local_addr().expect("addr").port();
let call_count = Arc::new(AtomicUsize::new(0));
let cc = Arc::clone(&call_count);
tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
break;
};
let cc = Arc::clone(&cc);
tokio::spawn(async move {
let io = hyper_util::rt::TokioIo::new(stream);
let svc = service_fn(move |_req: hyper::Request<hyper::body::Incoming>| {
let cc = Arc::clone(&cc);
async move {
let n = cc.fetch_add(1, Ordering::SeqCst);
let status = if n < fail_count { 503u16 } else { 200u16 };
let body = if n < fail_count {
Bytes::from("unavailable")
} else {
Bytes::from("ok")
};
Ok::<_, std::convert::Infallible>(
hyper::Response::builder()
.status(status)
.body(Full::new(body))
.expect("response build"),
)
}
});
let _ = http1::Builder::new().serve_connection(io, svc).await;
});
}
});
tokio::time::sleep(Duration::from_millis(5)).await;
(port, call_count)
}
#[tokio::test]
async fn test_retry_on_503_succeeds_on_third_attempt() {
let (port, call_count) = spawn_flaky_server(2).await;
let policy = oxihttp_client::RetryPolicy::new(3).with_backoff_base(Duration::from_millis(10));
let client = oxihttp_client::Client::builder()
.retry_policy(policy)
.build()
.expect("client build");
let url = format!("http://127.0.0.1:{port}/retry");
let resp = client
.get(&url)
.expect("builder")
.send()
.await
.expect("send");
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(call_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_no_retry_without_policy() {
let (port, call_count) = spawn_flaky_server(1).await;
let client = oxihttp_client::Client::builder()
.build()
.expect("client build");
let url = format!("http://127.0.0.1:{port}/no-retry");
let resp = client
.get(&url)
.expect("builder")
.send()
.await
.expect("send");
assert_eq!(resp.status().as_u16(), 503);
assert_eq!(call_count.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_retry_exhausted_returns_last_response() {
let (port, call_count) = spawn_flaky_server(100).await;
let policy = oxihttp_client::RetryPolicy::new(2).with_backoff_base(Duration::from_millis(5));
let client = oxihttp_client::Client::builder()
.retry_policy(policy)
.build()
.expect("client build");
let url = format!("http://127.0.0.1:{port}/always-503");
let resp = client
.get(&url)
.expect("builder")
.send()
.await
.expect("send");
assert_eq!(resp.status().as_u16(), 503);
assert_eq!(call_count.load(Ordering::SeqCst), 3);
}