#[cfg(not(target_arch = "wasm32"))]
use std::future::Future;
#[cfg(not(target_arch = "wasm32"))]
use std::pin::Pin;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{Arc, Mutex, MutexGuard};
#[cfg(not(target_arch = "wasm32"))]
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::error::{self, Error, Result};
#[derive(Clone)]
pub(crate) struct HttpClient {
client: reqwest::Client,
base_url: String,
api_key: String,
max_retries: u32,
request_timeout: Duration,
}
impl std::fmt::Debug for HttpClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpClient")
.field("base_url", &self.base_url)
.field("api_key", &"[redacted]")
.field("max_retries", &self.max_retries)
.field("request_timeout", &self.request_timeout)
.finish()
}
}
impl HttpClient {
pub(crate) fn new(
client: reqwest::Client,
base_url: String,
api_key: String,
max_retries: u32,
request_timeout: Duration,
) -> Self {
let base_url = base_url.trim_end_matches('/').to_string();
Self {
client,
base_url,
api_key,
max_retries,
request_timeout,
}
}
fn request(
&self,
method: reqwest::Method,
path: &str,
betas: &[String],
) -> reqwest::RequestBuilder {
let url = format!("{}{}", self.base_url, path);
let mut builder = self
.client
.request(method, url)
.header("x-api-key", self.api_key.as_str())
.header("anthropic-version", "2023-06-01")
.header(reqwest::header::CONTENT_TYPE, "application/json");
if !betas.is_empty() {
builder = builder.header("anthropic-beta", betas.join(","));
}
builder
}
pub(crate) async fn post_json<R: DeserializeOwned>(
&self,
path: &str,
body: &serde_json::Value,
betas: &[String],
) -> Result<R> {
let bytes = serde_json::to_vec(body)?;
let builder = self.request(reqwest::Method::POST, path, betas).body(bytes);
self.send_json(builder).await
}
pub(crate) async fn post_no_body<R: DeserializeOwned>(
&self,
path: &str,
betas: &[String],
) -> Result<R> {
let builder = self.request(reqwest::Method::POST, path, betas);
self.send_json(builder).await
}
pub(crate) async fn post_raw(
&self,
path: &str,
body: &serde_json::Value,
betas: &[String],
) -> Result<reqwest::Response> {
let bytes = serde_json::to_vec(body)?;
let builder = self.request(reqwest::Method::POST, path, betas).body(bytes);
self.execute(builder).await
}
pub(crate) async fn get_json<R: DeserializeOwned>(
&self,
path: &str,
betas: &[String],
) -> Result<R> {
let builder = self.request(reqwest::Method::GET, path, betas);
self.send_json(builder).await
}
pub(crate) async fn get_json_query<R: DeserializeOwned, Q: Serialize + ?Sized>(
&self,
path: &str,
query: &Q,
betas: &[String],
) -> Result<R> {
let builder = self.request(reqwest::Method::GET, path, betas).query(query);
self.send_json(builder).await
}
pub(crate) async fn get_raw(&self, path: &str, betas: &[String]) -> Result<reqwest::Response> {
let builder = self.request(reqwest::Method::GET, path, betas);
self.execute(builder).await
}
async fn send_json<R: DeserializeOwned>(&self, builder: reqwest::RequestBuilder) -> Result<R> {
let builder = builder.timeout(self.request_timeout);
let response = self.execute(builder).await?;
let bytes = response.bytes().await?;
serde_json::from_slice(&bytes).map_err(Error::from)
}
async fn execute(&self, builder: reqwest::RequestBuilder) -> Result<reqwest::Response> {
let mut attempt: u32 = 0;
loop {
let Some(attempt_builder) = builder.try_clone() else {
return Err(Error::Config(
"request body could not be cloned for retry".to_string(),
));
};
match attempt_builder.send().await {
Ok(response) if response.status().is_success() => return Ok(response),
Ok(response) => {
let err = map_error_response(response).await;
if attempt < self.max_retries && err.is_retryable() {
let delay = err.retry_after().unwrap_or_else(|| backoff_delay(attempt));
sleep(delay).await;
attempt += 1;
continue;
}
return Err(err);
}
Err(source) => {
let err = Error::from(source);
if attempt < self.max_retries && err.is_retryable() {
sleep(backoff_delay(attempt)).await;
attempt += 1;
continue;
}
return Err(err);
}
}
}
}
}
async fn map_error_response(response: reqwest::Response) -> Error {
let status = response.status().as_u16();
let request_id = header_string(response.headers(), "request-id");
const MAX_RETRY_AFTER_SECS: u64 = 60;
let retry_after = header_string(response.headers(), "retry-after")
.and_then(|s| s.trim().parse::<u64>().ok())
.filter(|secs| *secs <= MAX_RETRY_AFTER_SECS)
.map(Duration::from_secs);
match response.bytes().await {
Ok(bytes) => error::from_status(status, &bytes, request_id, retry_after),
Err(source) => Error::from(source),
}
}
fn header_string(headers: &reqwest::header::HeaderMap, name: &str) -> Option<String> {
headers
.get(name)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_string())
}
fn backoff_delay(attempt: u32) -> Duration {
const BASE_SECS: f64 = 0.5;
const CAP_SECS: f64 = 8.0;
let exponential = BASE_SECS * 2f64.powi(attempt as i32);
let ceiling = exponential.min(CAP_SECS);
Duration::from_secs_f64(ceiling * fastrand::f64())
}
async fn sleep(duration: Duration) {
#[cfg(target_arch = "wasm32")]
{
let _ = duration;
}
#[cfg(not(target_arch = "wasm32"))]
{
if duration.is_zero() {
return;
}
Timer::new(duration).await;
}
}
#[cfg(not(target_arch = "wasm32"))]
struct TimerState {
done: bool,
waker: Option<Waker>,
}
#[cfg(not(target_arch = "wasm32"))]
struct Timer {
state: Arc<Mutex<TimerState>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl Timer {
fn new(duration: Duration) -> Self {
let state = Arc::new(Mutex::new(TimerState {
done: false,
waker: None,
}));
let thread_state = Arc::clone(&state);
std::thread::spawn(move || {
std::thread::sleep(duration);
let mut guard = lock(&thread_state);
guard.done = true;
if let Some(waker) = guard.waker.take() {
waker.wake();
}
});
Self { state }
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Future for Timer {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let mut guard = lock(&self.state);
if guard.done {
Poll::Ready(())
} else {
guard.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
match mutex.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}