use crate::{Client, Error};
use reqwest::{Method, StatusCode};
use serde::Serialize;
use serde::de::DeserializeOwned;
mod agent;
mod api_keys;
mod attachments;
mod auth;
mod domains;
mod drafts;
mod inbox_events;
mod inboxes;
mod lists;
mod messages;
mod metrics;
mod organizations;
mod pods;
pub(crate) mod scope;
mod threads;
mod webhooks;
type NoBody = ();
impl Client {
pub(crate) async fn request<T, B>(
&self,
method: Method,
path: &str,
query: &[(&str, String)],
body: Option<&B>,
) -> Result<T, Error>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
let (status, text) = self.execute(method, path, query, body).await?;
if !status.is_success() {
return Err(Error::Api { status, body: text });
}
let text = if text.trim().is_empty() {
"null"
} else {
&text
};
serde_json::from_str(text).map_err(|e| Error::Decode {
reason: e.to_string(),
body: text.to_string(),
})
}
pub(crate) async fn request_text(&self, method: Method, path: &str) -> Result<String, Error> {
let (status, text) = self.execute(method, path, &[], None::<&NoBody>).await?;
if !status.is_success() {
return Err(Error::Api { status, body: text });
}
Ok(text)
}
fn build_request<B>(
&self,
method: Method,
url: &str,
query: &[(&str, String)],
body: Option<&B>,
) -> reqwest::RequestBuilder
where
B: Serialize + ?Sized,
{
let mut req = self.http.request(method, url).bearer_auth(&self.api_key);
if !query.is_empty() {
req = req.query(query);
}
if let Some(body) = body {
req = req.json(body);
}
req
}
#[cfg(feature = "retries")]
async fn execute<B>(
&self,
method: Method,
path: &str,
query: &[(&str, String)],
body: Option<&B>,
) -> Result<(StatusCode, String), Error>
where
B: Serialize + ?Sized,
{
let url = format!("{}{path}", self.base_url);
let policy = &self.retry_policy;
let mut attempt: u32 = 0;
loop {
match self
.build_request(method.clone(), &url, query, body)
.send()
.await
{
Ok(resp) => {
let status = resp.status();
let retry_after = parse_retry_after(resp.headers());
let text = resp.text().await.unwrap_or_default();
if is_retryable_status(status) && attempt < policy.max_retries {
let delay = retry_after.unwrap_or_else(|| policy.backoff(attempt));
tokio::time::sleep(delay).await;
attempt += 1;
continue;
}
return Ok((status, text));
}
Err(e) => {
if is_retryable_error(&e) && attempt < policy.max_retries {
tokio::time::sleep(policy.backoff(attempt)).await;
attempt += 1;
continue;
}
return Err(Error::Transport(e));
}
}
}
}
#[cfg(not(feature = "retries"))]
async fn execute<B>(
&self,
method: Method,
path: &str,
query: &[(&str, String)],
body: Option<&B>,
) -> Result<(StatusCode, String), Error>
where
B: Serialize + ?Sized,
{
let url = format!("{}{path}", self.base_url);
let resp = self.build_request(method, &url, query, body).send().await?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
Ok((status, text))
}
}
#[cfg(feature = "retries")]
impl crate::RetryPolicy {
fn backoff(&self, attempt: u32) -> std::time::Duration {
let base = self.base_delay.as_millis() as u64;
let cap = self.max_delay.as_millis() as u64;
let grown = base.saturating_mul(1u64 << attempt.min(20)).min(cap).max(1);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(0);
let jitter = nanos % (grown / 4 + 1);
std::time::Duration::from_millis(grown + jitter)
}
}
#[cfg(feature = "retries")]
fn is_retryable_status(status: StatusCode) -> bool {
status == StatusCode::REQUEST_TIMEOUT
|| status == StatusCode::TOO_MANY_REQUESTS
|| status.is_server_error()
}
#[cfg(feature = "retries")]
fn is_retryable_error(e: &reqwest::Error) -> bool {
e.is_timeout() || e.is_connect()
}
#[cfg(feature = "retries")]
fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<std::time::Duration> {
let secs: u64 = headers
.get(reqwest::header::RETRY_AFTER)?
.to_str()
.ok()?
.trim()
.parse()
.ok()?;
Some(std::time::Duration::from_secs(secs.min(30)))
}