use crate::retry::RetryPolicy;
use reqwest::Client;
use serde::Serialize;
use std::time::Duration;
pub struct HttpClient {
inner: Client,
}
impl HttpClient {
pub fn new() -> Self {
let inner = Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("failed to initialise reqwest client — TLS backend unavailable");
Self { inner }
}
pub fn with_reqwest(client: Client) -> Self {
Self { inner: client }
}
pub async fn post_json(
&self,
url: &str,
body: &impl Serialize,
) -> Result<reqwest::Response, reqwest::Error> {
#[cfg(not(feature = "tracing"))]
{
return self.inner.post(url).json(body).send().await;
}
#[cfg(feature = "tracing")]
{
use tracing::Instrument;
let span = tracing::info_span!("hooksmith.post_json", url = %url);
let start = std::time::Instant::now();
let result = self
.inner
.post(url)
.json(body)
.send()
.instrument(span.clone())
.await;
let latency_ms = start.elapsed().as_millis();
let _enter = span.enter();
match &result {
Ok(resp) => tracing::info!(status = resp.status().as_u16(), latency_ms),
Err(err) => tracing::error!(error = %err, latency_ms),
}
result
}
}
pub async fn post_json_with_retry(
&self,
url: &str,
body: &impl Serialize,
policy: &RetryPolicy,
) -> Result<reqwest::Response, reqwest::Error> {
let max = policy.max_attempts.max(1);
let mut last_err: Option<reqwest::Error> = None;
for attempt in 0..max {
match self.post_json(url, body).await {
Ok(resp) => return Ok(resp),
Err(err) => {
let is_last = attempt + 1 >= max;
if !is_last {
let factor = 1u32 << attempt; let base = policy.base_delay * factor;
let delay = if policy.jitter {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
let jitter = (nanos % 1_000) as f64 / 1_000.0;
base + Duration::from_secs_f64(base.as_secs_f64() * jitter)
} else {
base
};
tokio::time::sleep(delay).await;
}
last_err = Some(err);
}
}
}
Err(last_err.expect("max_attempts is at least 1"))
}
pub fn inner(&self) -> &Client {
&self.inner
}
}
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
}