reqwest-rest 0.1.0

An opinionated utility to help with creating (and configuring) one-off idiomatic REST API clients.
Documentation
use reqwest::StatusCode;
use reqwest_retry::{Retryable, RetryableStrategy, default_on_request_failure};

/// reqwest_retry helpfully logs something when a request is being retried:
///
/// https://docs.rs/reqwest-retry/0.5.0/src/reqwest_retry/middleware.rs.html#142
///
/// This includes how many times it's been retried and how long it will wait.
///
/// But it doesn't log *why* it was retried unfortunately.
///
/// I think the easiest way to make it log that is to wrap the RetryStrategy,
/// which is the component that decides when an error is retriable. Whenever
/// it says that the error is a Transient error (and should be retried), we can
/// log the error details.
/// The wrapped RetryStrategy can then be dropped in to the RetryTransientMiddleware.
///
/// Note: We deviated from DefaultRetryableStrategy because we don't consider 429 retryable.
#[derive(Default)]
pub struct LoggingRetryableStrategy {}

impl RetryableStrategy for LoggingRetryableStrategy {
    fn handle(
        &self,
        res: &Result<reqwest::Response, reqwest_middleware::Error>,
    ) -> Option<Retryable> {
        let result = match res {
            Ok(resp) => on_request_success(resp),
            Err(err) => default_on_request_failure(err),
        };

        if let Some(Retryable::Transient) = &result {
            match res {
                Ok(resp) => {
                    #[cfg(feature = "tracing")]
                    tracing::warn!(
                        "Retrying a transient error: response status code = {}",
                        resp.status()
                    )
                }
                Err(err) => {
                    #[cfg(feature = "tracing")]
                    tracing::warn!("Retrying a transient error: {err}")
                }
            }
        }

        result
    }
}

// Variation on upstream reqwest-middleware default_on_request_success function
// What we changed is, 429 TOO MANY REQUESTS should not be retried automatically, because it means we are being rate limited.
// Higher-level logic should decide if we should back off now for several minutes and try again, or forget it
// and do something else.
//
// https://github.com/TrueLayer/reqwest-middleware/blob/0b7624097964ce9752bd9fbf06618848edd3150e/reqwest-retry/src/retryable_strategy.rs#L113

fn on_request_success(success: &reqwest::Response) -> Option<Retryable> {
    let status = success.status();
    if status.is_success() {
        None
    } else if status.is_server_error() {
        Some(Retryable::Transient)
    } else if status == StatusCode::REQUEST_TIMEOUT {
        // 400-level error but maybe retriable
        Some(Retryable::Transient)
    } else {
        Some(Retryable::Fatal)
    }
}