use tracing::warn;
use crate::error::ApiError;
pub(crate) struct PostConfig {
pub use_query_key: bool,
pub auth_header: Option<&'static str>,
pub extra_headers: &'static [(&'static str, &'static str)],
pub max_retries: u32,
pub retry_delay_ms: u64,
}
pub(crate) async fn post_streaming<T: serde::Serialize>(
http: &reqwest::Client,
url: &str,
body: &T,
token: &str,
cfg: &PostConfig,
) -> Result<reqwest::Response, ApiError> {
let use_query_key = cfg.use_query_key;
let auth_header = cfg.auth_header;
let extra_headers = cfg.extra_headers;
let max_retries = cfg.max_retries;
let retry_delay_ms = cfg.retry_delay_ms;
let effective_url = if use_query_key {
if url.contains('?') {
format!("{}&key={}", url, token)
} else {
format!("{}?key={}", url, token)
}
} else {
url.to_string()
};
let mut attempts = 0u32;
let mut delay = retry_delay_ms;
loop {
let mut builder = http.post(&effective_url);
if !use_query_key {
match auth_header {
Some(h) => {
builder = builder.header(h, token);
}
None => {
builder = builder.bearer_auth(token);
}
}
}
for &(name, value) in extra_headers {
builder = builder.header(name, value);
}
#[cfg(feature = "sensitive-logs")]
if crate::sensitive_logs_enabled() {
let request_body = serde_json::to_string(body)
.unwrap_or_else(|e| format!("<failed to serialize body: {e}>"));
tracing::info!(
url = %effective_url,
body = %request_body,
"sending POST request"
);
}
builder = builder.json(body);
match builder.send().await.map_err(ApiError::Network) {
Ok(resp) if resp.status().is_success() => {
tracing::info!(
url = %effective_url,
status = %resp.status(),
"received streaming HTTP response"
);
return Ok(resp);
}
Ok(resp) => {
let status = resp.status();
let b = resp.text().await.unwrap_or_else(|e| e.to_string());
let err = ApiError::http(status, b);
if err.is_retriable() && attempts < max_retries {
attempts += 1;
warn!(error = %err, attempt = attempts, "transient error, retrying in {}ms", delay);
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
delay *= 2;
} else {
return Err(err);
}
}
Err(e) => {
if e.is_retriable() && attempts < max_retries {
attempts += 1;
warn!(error = %e, attempt = attempts, "transient error, retrying in {}ms", delay);
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
delay *= 2;
} else {
return Err(e);
}
}
}
}
}
pub(crate) async fn post_json<T: serde::Serialize>(
http: &reqwest::Client,
url: &str,
body: &T,
token: &str,
cfg: &PostConfig,
) -> Result<String, ApiError> {
let resp = post_streaming(http, url, body, token, cfg).await?;
let response_body = resp.text().await.map_err(ApiError::Network)?;
#[cfg(feature = "sensitive-logs")]
if crate::sensitive_logs_enabled() {
tracing::info!(url = %url, body = %response_body, "received raw HTTP response body");
}
Ok(response_body)
}