use crate::model::AppError; pub struct IonosClient { base_url: String, token: String, http: reqwest::blocking::Client, } fn expand_tilde(p: &str) -> String { if let Some(rest) = p.strip_prefix("~/") { if let Some(home) = std::env::var_os("HOME") { return format!("{}/{}", home.to_string_lossy(), rest); } } p.to_string() } impl IonosClient { pub fn new(base_url: &str, token_path: &str) -> Result { let path = expand_tilde(token_path); let token = std::fs::read_to_string(&path) .map_err(|e| AppError::Config(format!("token {path}: {e}")))? .trim().to_string(); if token.is_empty() { return Err(AppError::Config(format!("empty token at {path}"))); } let http = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(60)) .build() .map_err(|e| AppError::Ionos(format!("client: {e}")))?; Ok(Self { base_url: base_url.trim_end_matches('/').to_string(), token, http }) } /// POST with up to 3 retries on transport / 5xx, exponential backoff. pub fn post_json( &self, path: &str, body: &serde_json::Value, ) -> Result { let url = format!("{}{}", self.base_url, path); let mut last = String::new(); for attempt in 0..3 { let r = self.http.post(&url) .bearer_auth(&self.token) .json(body) .send(); match r { Ok(resp) if resp.status().is_success() => { return resp.json::() .map_err(|e| AppError::Ionos(format!("decode: {e}"))); } Ok(resp) => { last = format!("HTTP {}", resp.status()); if !resp.status().is_server_error() { return Err(AppError::Ionos(last)); } } Err(e) => last = format!("transport: {e}"), } std::thread::sleep(std::time::Duration::from_millis(300 * (1 << attempt))); } Err(AppError::Ionos(format!("giving up after retries: {last}"))) } }