use std::time::Duration;
use serde::Serialize;
use crate::api::engine_error::{VibeEngineError, VibeEngineErrorCode};
use crate::net::error::{map_reqwest_error, serialize_error};
use crate::net::response::VibeHttpResponse;
use crate::net::retry::VibeRetryPolicy;
pub struct VibeHttpRequest {
method: reqwest::Method,
url: String,
builder: reqwest::RequestBuilder,
retry: VibeRetryPolicy,
}
impl VibeHttpRequest {
pub(crate) fn new(
client: reqwest::Client,
method: reqwest::Method,
url: String,
retry: VibeRetryPolicy,
) -> Self {
let builder = client.request(method.clone(), &url);
Self {
method,
url,
builder,
retry,
}
}
fn context_label(&self) -> String {
context_label(&self.method, &self.url)
}
pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
self.builder = self.builder.header(key.as_ref(), value.as_ref());
self
}
pub fn query(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
self.builder = self.builder.query(&[(key.as_ref(), value.as_ref())]);
self
}
pub fn bearer_auth(mut self, token: impl Into<String>) -> Self {
self.builder = self.builder.bearer_auth(token.into());
self
}
pub fn basic_auth(mut self, username: impl Into<String>, password: Option<String>) -> Self {
self.builder = self.builder.basic_auth(username.into(), password);
self
}
pub fn json<T: Serialize>(mut self, value: &T) -> Result<Self, VibeEngineError> {
let context = self.context_label();
let body = serde_json::to_vec(value).map_err(|err| serialize_error(&context, err))?;
self.builder = self
.builder
.header("content-type", "application/json")
.body(body);
Ok(self)
}
pub fn body_bytes(mut self, body: Vec<u8>) -> Self {
self.builder = self.builder.body(body);
self
}
pub fn body_text(mut self, body: impl Into<String>) -> Self {
self.builder = self.builder.body(body.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.builder = self.builder.timeout(timeout);
self
}
pub fn retry(mut self, retry: VibeRetryPolicy) -> Self {
self.retry = retry;
self
}
pub async fn send(self) -> Result<VibeHttpResponse, VibeEngineError> {
let VibeHttpRequest {
method,
url,
builder,
retry,
} = self;
let context = context_label(&method, &url);
let max = retry.max_retries_value();
let mut attempt: u32 = 0;
loop {
let attempt_builder = builder.try_clone().ok_or_else(|| {
VibeEngineError::from_error_code(VibeEngineErrorCode::RequestError)
.with_source("request body is not retryable (stream)")
.with_context(context.clone())
})?;
match attempt_builder.send().await {
Ok(resp) => {
let status = resp.status().as_u16();
if attempt < max && retry.should_retry_status(status) {
let wait = backoff_with_retry_after(&retry, attempt, &resp);
sleep_backoff(wait).await;
attempt += 1;
continue;
}
return Ok(VibeHttpResponse::new(resp, context));
}
Err(err) => {
let retryable = (err.is_timeout() || err.is_connect())
&& !crate::net::error::is_tls_error(&err);
if attempt < max && retryable {
let base = retry.backoff_for(attempt);
let wait = retry.apply_jitter(base, seed(attempt));
sleep_backoff(wait).await;
attempt += 1;
continue;
}
return Err(map_reqwest_error(&context, err));
}
}
}
}
}
fn seed(attempt: u32) -> u64 {
(crate::platform::now() as u64) ^ (attempt as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
}
fn context_label(method: &reqwest::Method, url: &str) -> String {
format!("http {} {}", method.as_str(), url)
}
fn backoff_with_retry_after(
retry: &VibeRetryPolicy,
attempt: u32,
resp: &reqwest::Response,
) -> Duration {
if let Some(value) = resp.headers().get("retry-after") {
if let Ok(text) = value.to_str() {
if let Ok(secs) = text.trim().parse::<u64>() {
return Duration::from_secs(secs).min(retry.max_backoff_value());
}
}
}
let base = retry.backoff_for(attempt);
retry.apply_jitter(base, seed(attempt))
}
async fn sleep_backoff(duration: Duration) {
if !duration.is_zero() {
tokio::time::sleep(duration).await;
}
}
#[cfg(test)]
mod strict_tests {
use super::*;
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test/unit/net/request_tests.rs"
));
}