use crate::http_retry;
#[non_exhaustive] pub enum Auth {
None,
Bearer(String),
Header {
name: String,
value: String,
},
}
impl std::fmt::Debug for Auth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => f.write_str("None"),
Self::Bearer(_) => f.write_str("Bearer(<redacted>)"),
Self::Header { name, .. } => {
write!(f, "Header {{ name: {name:?}, value: <redacted> }}")
}
}
}
}
pub(crate) struct HttpFailure {
pub url: String,
pub attempts: u32,
pub cause: String,
}
pub struct HttpJsonClient {
base_url: String,
auth: Auth,
agent: ureq::Agent,
}
impl std::fmt::Debug for HttpJsonClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpJsonClient")
.field("base_url", &self.base_url)
.field("auth", &self.auth)
.finish_non_exhaustive()
}
}
impl HttpJsonClient {
#[must_use]
pub fn new(base_url: impl Into<String>, auth: Auth, agent: ureq::Agent) -> Self {
Self {
base_url: base_url.into().trim_end_matches('/').to_owned(),
auth,
agent,
}
}
pub(crate) fn url_for(&self, path: &str) -> String {
format!("{}{path}", self.base_url)
}
pub(crate) fn post_json(&self, path: &str, body: &str) -> Result<String, HttpFailure> {
let url = self.url_for(path);
let attempt = || {
let response = self
.authenticated(self.agent.post(&url))
.set("Content-Type", "application/json")
.send_string(body)
.map_err(|err| Call::Transport(Box::new(err)))?;
response.into_string().map_err(Call::Body)
};
http_retry::with_retry(&http_retry::HTTP_RETRIES, call_is_retryable, attempt).map_err(
|(err, attempts)| HttpFailure {
url,
attempts,
cause: match err {
Call::Transport(inner) => inner.to_string(),
Call::Body(inner) => format!("reading the response failed: {inner}"),
},
},
)
}
fn authenticated(&self, request: ureq::Request) -> ureq::Request {
match &self.auth {
Auth::None => request,
Auth::Bearer(token) => request.set("Authorization", &format!("Bearer {token}")),
Auth::Header { name, value } => request.set(name, value),
}
}
}
enum Call {
Transport(Box<ureq::Error>),
Body(std::io::Error),
}
fn call_is_retryable(err: &Call) -> bool {
match err {
Call::Transport(inner) => http_retry::is_retryable(inner),
Call::Body(inner) => http_retry::io_is_retryable(inner),
}
}
#[derive(Debug, Clone, Copy)]
pub struct AgentBudget {
pub connect: std::time::Duration,
pub write: std::time::Duration,
pub overall: std::time::Duration,
}
impl AgentBudget {
#[must_use]
pub const fn local_daemon(overall: std::time::Duration) -> Self {
Self {
connect: std::time::Duration::from_secs(2),
write: std::time::Duration::from_secs(10),
overall,
}
}
#[must_use]
pub const fn uniform(budget: std::time::Duration) -> Self {
Self {
connect: budget,
write: budget,
overall: budget,
}
}
}
#[must_use]
pub fn bounded_agent(budget: AgentBudget) -> ureq::Agent {
ureq::AgentBuilder::new()
.timeout_connect(budget.connect)
.timeout_write(budget.write)
.timeout_read(budget.overall)
.timeout(budget.overall)
.build()
}
#[cfg(test)]
#[path = "http_client_tests.rs"]
mod tests;