use std::sync::{Arc, OnceLock};
use std::time::Duration;
use systemprompt_models::net::{HTTP_AUTH_VERIFY_TIMEOUT, HTTP_CONNECT_TIMEOUT};
use super::error::CredentialError;
const RETRY_BACKOFF: Duration = Duration::from_millis(250);
fn client() -> Result<&'static reqwest::Client, CredentialError> {
static CLIENT: OnceLock<Result<reqwest::Client, Arc<reqwest::Error>>> = OnceLock::new();
CLIENT
.get_or_init(|| {
reqwest::Client::builder()
.connect_timeout(HTTP_CONNECT_TIMEOUT)
.timeout(HTTP_AUTH_VERIFY_TIMEOUT)
.build()
.map_err(Arc::new)
})
.as_ref()
.map_err(|e| CredentialError::Client(Arc::clone(e)))
}
pub(crate) async fn post_form(
uri: &str,
form: &[(&str, String)],
) -> Result<String, CredentialError> {
send_with_retry(uri, |client| client.post(uri).form(form)).await
}
pub(crate) async fn post_bearer(
uri: &str,
bearer: &str,
headers: &[(&str, &str)],
) -> Result<String, CredentialError> {
send_with_retry(uri, |client| {
headers.iter().fold(
client.post(uri).bearer_auth(bearer),
|request, (name, value)| request.header(*name, *value),
)
})
.await
}
async fn send_with_retry<B>(uri: &str, build: B) -> Result<String, CredentialError>
where
B: Fn(&reqwest::Client) -> reqwest::RequestBuilder,
{
let client = client()?;
let mut attempt = 0;
loop {
let outcome = attempt_send(build(client)).await;
let retryable = matches!(
outcome,
Err(Retryable::Transport(_) | Retryable::Unavailable { .. })
);
if retryable && attempt == 0 {
attempt += 1;
tokio::time::sleep(RETRY_BACKOFF).await;
continue;
}
return match outcome {
Ok(body) => Ok(body),
Err(Retryable::Transport(source)) => Err(CredentialError::Unreachable {
uri: uri.to_owned(),
source,
}),
Err(Retryable::Unavailable { status, body } | Retryable::Refused { status, body }) => {
Err(CredentialError::Rejected { status, body })
},
};
}
}
enum Retryable {
Transport(reqwest::Error),
Unavailable { status: String, body: String },
Refused { status: String, body: String },
}
async fn attempt_send(request: reqwest::RequestBuilder) -> Result<String, Retryable> {
let response = request.send().await.map_err(Retryable::Transport)?;
let status = response.status();
let body = response.text().await.map_err(Retryable::Transport)?;
if status.is_success() {
return Ok(body);
}
let status_text = status.to_string();
let body = body.trim().to_owned();
if status.is_server_error() {
return Err(Retryable::Unavailable {
status: status_text,
body,
});
}
Err(Retryable::Refused {
status: status_text,
body,
})
}