use std::time::Duration;
#[derive(Debug)]
pub enum SendOutcome {
Success,
RateLimited(Duration),
Error(String),
}
pub trait Transport: Send + Sync {
fn send(&self, body: &[u8]) -> SendOutcome;
}
pub struct UreqTransport {
agent: ureq::Agent,
endpoint: String,
api_key: String,
user_agent: String,
}
impl UreqTransport {
pub fn new(endpoint: String, api_key: String, timeout: Duration) -> Self {
let agent = ureq::AgentBuilder::new()
.timeout_connect(timeout)
.timeout_read(timeout)
.timeout_write(timeout)
.build();
UreqTransport {
agent,
endpoint,
api_key,
user_agent: format!("errsight-rust/{}", crate::VERSION),
}
}
}
impl Transport for UreqTransport {
fn send(&self, body: &[u8]) -> SendOutcome {
let result = self
.agent
.post(&self.endpoint)
.set("Content-Type", "application/json")
.set("X-API-Key", &self.api_key)
.set("User-Agent", &self.user_agent)
.send_bytes(body);
match result {
Ok(resp) => {
let code = resp.status();
if (200..300).contains(&code) {
SendOutcome::Success
} else {
SendOutcome::Error(format!("unexpected status {code}"))
}
}
Err(ureq::Error::Status(code, resp)) => {
if code == 429 {
let retry_after = resp
.header("Retry-After")
.and_then(|h| h.trim().parse::<u64>().ok())
.unwrap_or(60)
.min(600);
SendOutcome::RateLimited(Duration::from_secs(retry_after))
} else {
let snippet = resp
.into_string()
.map(|s| s.chars().take(200).collect::<String>())
.unwrap_or_default();
SendOutcome::Error(format!("API error {code}: {snippet}"))
}
}
Err(ureq::Error::Transport(t)) => SendOutcome::Error(format!("transport error: {t}")),
}
}
}