use std::time::Duration;
use bytes::Bytes;
use reqwest::redirect::Policy;
use reqwest::Client;
use crate::error::{
OlError, OL_4223_VERDICT_TOO_LARGE, OL_4224_TOOL_UNREACHABLE, OL_4225_TOOL_5XX,
OL_4228_DEADLINE_EXCEEDED,
};
use crate::runtime::deadline::Deadline;
use crate::runtime::multi_tool::LocalRoute;
pub const MAX_VERDICT_BYTES: usize = 250 * 1024;
pub fn build_proxy_client() -> Result<Client, OlError> {
Client::builder()
.pool_idle_timeout(Duration::from_secs(90))
.pool_max_idle_per_host(32)
.redirect(Policy::none())
.build()
.map_err(|e| OlError::new(OL_4224_TOOL_UNREACHABLE, format!("reqwest builder: {e}")))
}
#[derive(Debug, Clone)]
pub struct ProxyOutcome {
pub body: Bytes,
pub tool_ms: u64,
}
pub async fn proxy(
client: &Client,
route: &LocalRoute,
body: Bytes,
deadline: Deadline,
) -> Result<ProxyOutcome, OlError> {
let started = std::time::Instant::now();
let remaining = deadline.remaining()?;
let request = client
.post(&route.local_url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(body);
let resp = match tokio::time::timeout(remaining, request.send()).await {
Err(_) => {
return Err(OlError::new(
OL_4228_DEADLINE_EXCEEDED,
"deadline elapsed waiting for localhost tool",
));
}
Ok(Err(e)) if e.is_connect() => {
return Err(OlError::new(
OL_4224_TOOL_UNREACHABLE,
format!("connect to {}: {}", route.local_url, e),
));
}
Ok(Err(e)) if e.is_timeout() => {
return Err(OlError::new(
OL_4228_DEADLINE_EXCEEDED,
format!("reqwest timeout: {e}"),
));
}
Ok(Err(e)) => {
return Err(OlError::new(
OL_4224_TOOL_UNREACHABLE,
format!("reqwest error: {e}"),
));
}
Ok(Ok(r)) => r,
};
if !resp.status().is_success() {
let status = resp.status();
return Err(OlError::new(
OL_4225_TOOL_5XX,
format!("tool returned HTTP {status}"),
));
}
if let Some(cl) = resp.content_length() {
if cl as usize > MAX_VERDICT_BYTES {
return Err(OlError::new(
OL_4223_VERDICT_TOO_LARGE,
format!("Content-Length {cl} > {MAX_VERDICT_BYTES} cap"),
));
}
}
let bytes = match tokio::time::timeout(deadline.remaining()?, resp.bytes()).await {
Err(_) => {
return Err(OlError::new(
OL_4228_DEADLINE_EXCEEDED,
"deadline elapsed reading tool body",
));
}
Ok(Err(e)) => {
return Err(OlError::new(
OL_4225_TOOL_5XX,
format!("read tool body: {e}"),
));
}
Ok(Ok(b)) => b,
};
if bytes.len() > MAX_VERDICT_BYTES {
return Err(OlError::new(
OL_4223_VERDICT_TOO_LARGE,
format!("tool body {} bytes > {MAX_VERDICT_BYTES} cap", bytes.len()),
));
}
Ok(ProxyOutcome {
body: bytes,
tool_ms: started.elapsed().as_millis() as u64,
})
}
pub fn telemetry_kind(err: &OlError) -> &'static str {
match err.code.code {
"OL-4223" => "oversize",
"OL-4224" => "unreachable",
"OL-4225" => "5xx",
"OL-4228" => "timeout",
_ => "unreachable",
}
}