camber 0.1.6

Opinionated async Rust for IO-bound services on top of Tokio
Documentation
mod common;

use camber::http::{self, Request, Response, Router};
use camber::{RuntimeError, runtime};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;

#[camber::test]
async fn client_retries_on_transient_error() {
    let count = Arc::new(AtomicU32::new(0));
    let c = Arc::clone(&count);
    let mut backend = Router::new();
    backend.get("/retry", move |_req: &Request| {
        let n = c.fetch_add(1, Ordering::Relaxed);
        async move {
            match n < 2 {
                true => Response::empty(503),
                false => Response::text(200, "ok"),
            }
        }
    });
    let addr = common::spawn_server(backend);

    let resp = http::client()
        .retries(3)
        .backoff(Duration::from_millis(10))
        .get(&format!("http://{addr}/retry"))
        .await
        .unwrap();

    assert_eq!(resp.status(), 200);
    assert_eq!(resp.body(), "ok");
    assert_eq!(count.load(Ordering::Relaxed), 3);

    runtime::request_shutdown();
}

#[camber::test]
async fn client_does_not_retry_on_4xx() {
    let count = Arc::new(AtomicU32::new(0));
    let c = Arc::clone(&count);
    let mut backend = Router::new();
    backend.get("/bad", move |_req: &Request| {
        c.fetch_add(1, Ordering::Relaxed);
        async { Response::text(400, "bad request") }
    });
    let addr = common::spawn_server(backend);

    let resp = http::client()
        .retries(3)
        .backoff(Duration::from_millis(10))
        .get(&format!("http://{addr}/bad"))
        .await
        .unwrap();

    assert_eq!(resp.status(), 400);
    assert_eq!(count.load(Ordering::Relaxed), 1);

    runtime::request_shutdown();
}

#[camber::test]
async fn client_exhausts_retries_and_returns_last_error() {
    let count = Arc::new(AtomicU32::new(0));
    let c = Arc::clone(&count);
    let mut backend = Router::new();
    backend.get("/fail", move |_req: &Request| {
        c.fetch_add(1, Ordering::Relaxed);
        async { Response::empty(503) }
    });
    let addr = common::spawn_server(backend);

    let resp = http::client()
        .retries(2)
        .backoff(Duration::from_millis(10))
        .get(&format!("http://{addr}/fail"))
        .await
        .unwrap();

    assert_eq!(resp.status(), 503);
    assert_eq!(count.load(Ordering::Relaxed), 3);

    runtime::request_shutdown();
}

#[camber::test]
async fn client_free_functions_do_not_retry() {
    let count = Arc::new(AtomicU32::new(0));
    let c = Arc::clone(&count);
    let mut backend = Router::new();
    backend.get("/once", move |_req: &Request| {
        c.fetch_add(1, Ordering::Relaxed);
        async { Response::empty(503) }
    });
    let addr = common::spawn_server(backend);

    let resp = http::get(&format!("http://{addr}/once")).await.unwrap();

    assert_eq!(resp.status(), 503);
    assert_eq!(count.load(Ordering::Relaxed), 1);

    runtime::request_shutdown();
}

#[camber::test]
async fn client_retries_on_timeout() {
    let count = Arc::new(AtomicU32::new(0));
    let c = Arc::clone(&count);
    let mut backend = Router::new();
    backend.get("/slow", move |_req: &Request| {
        c.fetch_add(1, Ordering::Relaxed);
        async {
            std::thread::sleep(Duration::from_millis(200));
            Response::text(200, "slow")
        }
    });
    let addr = common::spawn_server(backend);

    let result = http::client()
        .retries(1)
        .backoff(Duration::from_millis(10))
        .read_timeout(Duration::from_millis(50))
        .get(&format!("http://{addr}/slow"))
        .await;

    match &result {
        Err(RuntimeError::Timeout) => {}
        Err(e) => panic!("expected Timeout, got error: {e}"),
        Ok(resp) => panic!("expected Timeout, got status {}", resp.status()),
    }

    assert_eq!(count.load(Ordering::Relaxed), 2);

    runtime::request_shutdown();
}