use std::time::Duration; use anyhow::{Context, Result, anyhow}; use serde::Serialize; use ureq::SendBody; use ureq::http::{Method, Request}; const ATTEMPTS: u32 = 4; /// A JSON HTTP API that authenticates every request with the same headers. pub struct Api { agent: ureq::Agent, base: &'static str, headers: Vec<(&'static str, String)>, } impl Api { pub fn new(base: &'static str, headers: Vec<(&'static str, String)>) -> Self { let config = ureq::Agent::config_builder() .timeout_global(Some(Duration::from_secs(30))) .http_status_as_error(false) .build(); Api { agent: ureq::Agent::new_with_config(config), base, headers, } } /// Send a request, retrying transport errors, 429s and 5xxs with backoff. /// Returns the final status and body: every API words its errors /// differently, so judging them is left to the caller. pub fn call( &self, method: Method, path: &str, body: Option<&impl Serialize>, ) -> Result<(u16, String)> { let mut last_err = String::new(); for attempt in 0..ATTEMPTS { if attempt > 0 { std::thread::sleep(Duration::from_millis(500 << (attempt - 1))); } let mut request = Request::builder() .method(method.clone()) .uri(format!("{}{path}", self.base)); for (name, value) in &self.headers { request = request.header(*name, value); } let body = match body { Some(b) => SendBody::from_json(b)?, None => SendBody::none(), }; let mut response = match self.agent.run(request.body(body)?) { Ok(r) => r, Err(e) => { last_err = format!("{method} {path}: {e}"); continue; } }; let status = response.status().as_u16(); if (status == 429 || status >= 500) && attempt + 1 < ATTEMPTS { last_err = format!("{method} {path}: HTTP {status}"); continue; } let text = response .body_mut() .read_to_string() .with_context(|| format!("{method} {path}: unreadable response"))?; return Ok((status, text)); } Err(anyhow!(last_err)) } }