use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use reqwest::header::HeaderMap;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::error::{ApiError, Error, ErrorCode, Result};
use crate::response::Response;
#[derive(Debug, serde::Deserialize)]
struct RawEnvelope {
status: Option<String>,
request_id: Option<String>,
data: Option<Value>,
credits_used: Option<i64>,
credits_remaining: Option<i64>,
error: Option<Value>,
}
pub(crate) fn is_retryable_status(status: u16) -> bool {
matches!(status, 429 | 500 | 502 | 503)
}
pub(crate) fn header_str(headers: &HeaderMap, name: &str) -> Option<String> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
pub(crate) fn parse_retry_after(value: Option<&str>) -> Option<Duration> {
let secs: u64 = value?.trim().parse().ok()?;
Some(Duration::from_secs(secs.min(300)))
}
pub(crate) fn backoff_delay(attempt: u32, retry_after: Option<Duration>) -> Duration {
if let Some(ra) = retry_after {
return ra;
}
const BASE_MS: u64 = 1_000;
const CAP_MS: u64 = 30_000;
let exp = attempt.saturating_sub(1).min(20);
let window_ms = BASE_MS.saturating_mul(1u64 << exp).min(CAP_MS);
Duration::from_millis((window_ms as f64 * pseudo_random_unit()) as u64)
}
fn pseudo_random_unit() -> f64 {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let c = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut x = nanos
.wrapping_mul(6364136223846793005)
.wrapping_add(c.wrapping_mul(1442695040888963407))
.wrapping_add(1);
x ^= x >> 30;
x = x.wrapping_mul(0xbf58476d1ce4e5b9);
x ^= x >> 27;
x = x.wrapping_mul(0x94d049bb133111eb);
x ^= x >> 31;
(x >> 11) as f64 / (1u64 << 53) as f64
}
pub(crate) fn is_retryable_send_error(err: &reqwest::Error) -> bool {
err.is_connect()
}
pub(crate) fn process_response<T: DeserializeOwned>(
status: u16,
request_id_header: Option<String>,
retry_after: Option<Duration>,
body: &[u8],
) -> Result<Response<T>> {
let raw: Option<RawEnvelope> = serde_json::from_slice(body).ok();
let request_id = raw
.as_ref()
.and_then(|r| r.request_id.clone())
.or(request_id_header);
let is_error_envelope = raw
.as_ref()
.is_some_and(|r| r.status.as_deref() == Some("error") || r.error.is_some());
if status >= 400 || is_error_envelope {
return Err(Error::Api(build_api_error(
status,
request_id,
retry_after,
raw.as_ref(),
body,
)));
}
let raw = raw.ok_or_else(|| {
Error::Decode(format!(
"response body was not valid JSON (HTTP {status}, {} bytes)",
body.len()
))
})?;
let data_value = raw.data.unwrap_or(Value::Null);
let data = serde_json::from_value(data_value)
.map_err(|e| Error::Decode(format!("failed to decode response data: {e}")))?;
Ok(Response {
request_id,
credits_used: raw.credits_used,
credits_remaining: raw.credits_remaining,
data,
})
}
fn build_api_error(
status: u16,
request_id: Option<String>,
retry_after: Option<Duration>,
raw: Option<&RawEnvelope>,
_body: &[u8],
) -> ApiError {
let error_field = raw.and_then(|r| r.error.as_ref());
let (code_opt, message_opt, details) = match error_field {
Some(Value::Object(map)) => (
map.get("code").and_then(Value::as_str).map(str::to_string),
map.get("message")
.and_then(Value::as_str)
.map(str::to_string),
map.get("details").cloned(),
),
Some(Value::String(s)) => (None, Some(s.clone()), None),
_ => (None, None, None),
};
let code = match code_opt {
Some(c) => ErrorCode::parse(&c),
None if status == 401 => ErrorCode::Unauthorized,
None => ErrorCode::from_status(status),
};
let message =
message_opt.unwrap_or_else(|| format!("request failed with HTTP status {status}"));
ApiError::new(status, code, message, details, request_id, retry_after)
}