pub mod config;
pub use config::HttpClientConfig;
use reqwest::Response;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::RetryTransientMiddleware;
use reqwest_retry::policies::ExponentialBackoff;
#[derive(Debug, thiserror::Error)]
pub enum HttpClientError {
#[error("failed to build HTTP client: {0}")]
BuildError(#[from] reqwest::Error),
}
pub struct HttpClient {
inner: ClientWithMiddleware,
config: HttpClientConfig,
}
impl HttpClient {
#[must_use]
pub fn new(config: HttpClientConfig) -> Self {
Self::try_new(config).expect("failed to build reqwest client (TLS init failure)")
}
pub fn try_new(config: HttpClientConfig) -> Result<Self, HttpClientError> {
let retry_policy = ExponentialBackoff::builder()
.retry_bounds(
std::time::Duration::from_millis(config.min_retry_interval_ms),
std::time::Duration::from_millis(config.max_retry_interval_ms),
)
.build_with_max_retries(config.max_retries);
let mut builder = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(config.timeout_secs))
.connect_timeout(std::time::Duration::from_secs(config.connect_timeout_secs));
if let Some(ref ua) = config.user_agent {
builder = builder.user_agent(ua.clone());
}
let reqwest_client = builder.build()?;
let client = ClientBuilder::new(reqwest_client)
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
Ok(Self {
inner: client,
config,
})
}
#[must_use]
pub fn from_cascade() -> Self {
Self::new(HttpClientConfig::from_cascade())
}
pub async fn get(&self, url: &str) -> Result<Response, reqwest_middleware::Error> {
#[cfg(feature = "metrics")]
let start = std::time::Instant::now();
let result = self.inner.get(url).send().await;
#[cfg(feature = "metrics")]
{
let status = if result.is_ok() { "success" } else { "error" };
metrics::counter!("dfe_http_client_requests_total", "method" => "GET", "status" => status).increment(1);
metrics::histogram!("dfe_http_client_duration_seconds", "method" => "GET")
.record(start.elapsed().as_secs_f64());
}
result
}
pub async fn post_json<T: serde::Serialize + ?Sized>(
&self,
url: &str,
body: &T,
) -> Result<Response, reqwest_middleware::Error> {
#[cfg(feature = "metrics")]
let start = std::time::Instant::now();
let result = self
.inner
.post(url)
.header("content-type", "application/json")
.body(serde_json::to_vec(body).unwrap_or_default())
.send()
.await;
#[cfg(feature = "metrics")]
{
let status = if result.is_ok() { "success" } else { "error" };
metrics::counter!("dfe_http_client_requests_total", "method" => "POST", "status" => status).increment(1);
metrics::histogram!("dfe_http_client_duration_seconds", "method" => "POST")
.record(start.elapsed().as_secs_f64());
}
result
}
pub async fn put_json<T: serde::Serialize + ?Sized>(
&self,
url: &str,
body: &T,
) -> Result<Response, reqwest_middleware::Error> {
#[cfg(feature = "metrics")]
let start = std::time::Instant::now();
let result = self
.inner
.put(url)
.header("content-type", "application/json")
.body(serde_json::to_vec(body).unwrap_or_default())
.send()
.await;
#[cfg(feature = "metrics")]
{
let status = if result.is_ok() { "success" } else { "error" };
metrics::counter!("dfe_http_client_requests_total", "method" => "PUT", "status" => status).increment(1);
metrics::histogram!("dfe_http_client_duration_seconds", "method" => "PUT")
.record(start.elapsed().as_secs_f64());
}
result
}
pub async fn delete(&self, url: &str) -> Result<Response, reqwest_middleware::Error> {
#[cfg(feature = "metrics")]
let start = std::time::Instant::now();
let result = self.inner.delete(url).send().await;
#[cfg(feature = "metrics")]
{
let status = if result.is_ok() { "success" } else { "error" };
metrics::counter!("dfe_http_client_requests_total", "method" => "DELETE", "status" => status).increment(1);
metrics::histogram!("dfe_http_client_duration_seconds", "method" => "DELETE")
.record(start.elapsed().as_secs_f64());
}
result
}
#[must_use]
pub fn client(&self) -> &ClientWithMiddleware {
&self.inner
}
#[must_use]
pub fn config(&self) -> &HttpClientConfig {
&self.config
}
}