use std::error::Error;
use std::io::Cursor;
use std::time::Duration;
pub const MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
pub const MAX_ELEMENT_DEPTH: usize = 256;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ResponseRefusal {
TooLarge { seen: usize },
TooDeep { depth: usize },
Unscannable { detail: String },
}
impl ResponseRefusal {
pub fn decision_tag(&self) -> &'static str {
match self {
Self::TooLarge { .. } => "fail_closed_response_too_large",
Self::TooDeep { .. } => "fail_closed_response_too_deep",
Self::Unscannable { .. } => "fail_closed_response_unscannable",
}
}
}
impl std::fmt::Display for ResponseRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooLarge { seen } => write!(
f,
"server response exceeded {} bytes (stopped at {})",
MAX_RESPONSE_BYTES, seen
),
Self::TooDeep { depth } => write!(
f,
"server response nested deeper than {} XML elements (reached {})",
MAX_ELEMENT_DEPTH, depth
),
Self::Unscannable { detail } => write!(
f,
"server response could not be scanned for nesting depth: {}",
detail
),
}
}
}
impl std::error::Error for ResponseRefusal {}
pub fn scan_depth(body: &[u8]) -> Result<usize, ResponseRefusal> {
use quick_xml::events::Event;
let mut reader = quick_xml::Reader::from_reader(body);
reader.config_mut().check_end_names = false;
let mut buf = Vec::new();
let mut depth: usize = 0;
let mut deepest: usize = 0;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(_)) => {
depth += 1;
deepest = deepest.max(depth);
if depth > MAX_ELEMENT_DEPTH {
return Err(ResponseRefusal::TooDeep { depth });
}
}
Ok(Event::Empty(_)) => {
deepest = deepest.max(depth + 1);
if depth + 1 > MAX_ELEMENT_DEPTH {
return Err(ResponseRefusal::TooDeep { depth: depth + 1 });
}
}
Ok(Event::End(_)) => {
depth = depth.saturating_sub(1);
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(e) => {
return Err(ResponseRefusal::Unscannable {
detail: e.to_string(),
})
}
}
buf.clear();
}
Ok(deepest)
}
pub async fn read_body_capped(response: reqwest::Response) -> Result<Vec<u8>, anyhow::Error> {
let mut response = response;
let mut body: Vec<u8> = Vec::new();
while let Some(chunk) = response.chunk().await? {
if body.len() + chunk.len() > MAX_RESPONSE_BYTES {
return Err(ResponseRefusal::TooLarge {
seen: body.len() + chunk.len(),
}
.into());
}
body.extend_from_slice(&chunk);
}
Ok(body)
}
pub fn check_response(response: &reqwest::Response) -> Result<(), anyhow::Error> {
let status = response.status();
if status.is_client_error() || status.is_server_error() {
anyhow::bail!("server response indicates error: {}", status);
}
if let Some(value) = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
{
let essence = value
.split(';')
.next()
.unwrap_or("")
.trim()
.to_ascii_lowercase();
if let Some((ty, sub)) = essence.split_once('/') {
if !(ty == "text" && sub == "xml") {
anyhow::bail!("expected Content-Type 'text/xml', got '{}/{}'", ty, sub);
}
}
}
Ok(())
}
#[derive(Debug)]
pub struct PrefetchedTransport {
body: Vec<u8>,
}
impl PrefetchedTransport {
pub fn new(body: Vec<u8>) -> Self {
Self { body }
}
}
impl xmlrpc::Transport for PrefetchedTransport {
type Stream = Cursor<Vec<u8>>;
fn transmit(
self,
_request: &xmlrpc::Request<'_>,
) -> Result<Self::Stream, Box<dyn Error + Send + Sync>> {
Ok(Cursor::new(self.body))
}
}
pub async fn http_client_for(server_url: &str, timeout: Duration) -> reqwest::Client {
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<HashMap<(String, u64), reqwest::Client>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let key = (
crate::llm::ollama_client::host_of(server_url).to_string(),
timeout.as_secs(),
);
if let Ok(guard) = cache.lock() {
if let Some(client) = guard.get(&key) {
return client.clone();
}
}
let url = server_url.to_string();
let built = tokio::task::spawn_blocking(move || {
crate::llm::ollama_client::client_for_endpoint_with_timeout(&url, timeout)
})
.await
.unwrap_or_else(|_| reqwest::Client::new());
if let Ok(mut guard) = cache.lock() {
guard.insert(key, built.clone());
}
built
}