From 489d3646f843590a49e5639eff871696e3b3d36e Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Tue, 21 Jul 2026 19:02:07 +0200 Subject: [PATCH] Fix loopback test networking. --- crates/bds-core/src/engine/ai.rs | 41 ++++++++++++++++++----- crates/bds-core/src/engine/chat.rs | 32 +++++++++++++++--- crates/bds-core/src/engine/preview.rs | 19 +++++++---- crates/bds-core/tests/mcp.rs | 5 ++- crates/bds-ui/src/platform/script_host.rs | 7 ++-- 5 files changed, 83 insertions(+), 21 deletions(-) diff --git a/crates/bds-core/src/engine/ai.rs b/crates/bds-core/src/engine/ai.rs index d6cac62..b78935b 100644 --- a/crates/bds-core/src/engine/ai.rs +++ b/crates/bds-core/src/engine/ai.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::net::IpAddr; use std::sync::{LazyLock, Mutex}; use std::time::Duration; @@ -427,7 +428,7 @@ pub fn load_endpoint_api_key(kind: AiEndpointKind) -> EngineResult EngineResult> { validate_endpoint_access(endpoint)?; - let client = build_http_client()?; + let client = build_http_client_for_endpoint(&endpoint.url)?; let request = client.get(models_url(&endpoint.url)); let response = with_auth(request, endpoint).send()?.error_for_status()?; let body: Value = response.json()?; @@ -494,7 +495,7 @@ pub fn test_chat(endpoint: &AiEndpointConfig, model: &str) -> EngineResult<()> { "stream": false, }); with_auth( - build_http_client()? + build_http_client_for_endpoint(&endpoint.url)? .post(chat_completions_url(&endpoint.url)) .json(&payload), endpoint, @@ -535,7 +536,7 @@ pub fn run_one_shot( } } }); - let client = build_http_client()?; + let client = build_http_client_for_endpoint(&endpoint.url)?; let response = with_auth( client .post(chat_completions_url(&endpoint.url)) @@ -575,8 +576,28 @@ fn parse_token_usage(body: &Value) -> TokenUsage { } } -fn build_http_client() -> EngineResult { - Ok(Client::builder().timeout(Duration::from_secs(5)).build()?) +fn build_http_client_for_endpoint(endpoint_url: &str) -> EngineResult { + let builder = Client::builder().timeout(Duration::from_secs(5)); + let builder = if should_bypass_proxy(endpoint_url) { + builder.no_proxy() + } else { + builder + }; + Ok(builder.build()?) +} + +fn should_bypass_proxy(endpoint_url: &str) -> bool { + let Ok(url) = reqwest::Url::parse(endpoint_url) else { + return false; + }; + let Some(host) = url.host_str() else { + return false; + }; + host.eq_ignore_ascii_case("localhost") + || host + .parse::() + .map(|address| address.is_loopback()) + .unwrap_or(false) } fn load_endpoint(conn: &Connection, kind: AiEndpointKind) -> EngineResult { @@ -936,7 +957,7 @@ fn store_endpoint_api_key(kind: AiEndpointKind, api_key: &str) -> EngineResult<( TEST_API_KEYS .lock() .map_err(|error| EngineError::Validation(error.to_string()))? - .insert(kind.as_str().to_string(), api_key.to_string()); + .insert(test_api_key_name(kind), api_key.to_string()); return Ok(()); } endpoint_keyring_entry(kind)? @@ -950,7 +971,7 @@ fn read_endpoint_api_key(kind: AiEndpointKind) -> EngineResult> { .lock() .map_err(|error| EngineError::Validation(error.to_string())) .map(|keys| { - keys.get(kind.as_str()) + keys.get(&test_api_key_name(kind)) .cloned() .filter(|key| !key.trim().is_empty()) }); @@ -968,7 +989,7 @@ fn delete_endpoint_api_key(kind: AiEndpointKind) -> EngineResult<()> { TEST_API_KEYS .lock() .map_err(|error| EngineError::Validation(error.to_string()))? - .remove(kind.as_str()); + .remove(&test_api_key_name(kind)); return Ok(()); } match endpoint_keyring_entry(kind)?.delete_credential() { @@ -989,6 +1010,10 @@ fn cargo_test_process() -> bool { }) } +fn test_api_key_name(kind: AiEndpointKind) -> String { + format!("{:?}:{}", std::thread::current().id(), kind.as_str()) +} + fn keyring_error(error: keyring::Error) -> EngineError { EngineError::Validation(error.to_string()) } diff --git a/crates/bds-core/src/engine/chat.rs b/crates/bds-core/src/engine/chat.rs index bda3f13..a940659 100644 --- a/crates/bds-core/src/engine/chat.rs +++ b/crates/bds-core/src/engine/chat.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, HashMap}; use std::io::Read; +use std::net::IpAddr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock, mpsc}; use std::time::Duration; @@ -836,16 +837,39 @@ pub fn build_context( Ok(result) } +fn chat_http_client(endpoint_url: &str) -> EngineResult { + let builder = Client::builder() + .connect_timeout(Duration::from_secs(5)) + .timeout(Duration::from_secs(300)); + let builder = if should_bypass_proxy(endpoint_url) { + builder.no_proxy() + } else { + builder + }; + Ok(builder.build()?) +} + +fn should_bypass_proxy(endpoint_url: &str) -> bool { + let Ok(url) = reqwest::Url::parse(endpoint_url) else { + return false; + }; + let Some(host) = url.host_str() else { + return false; + }; + host.eq_ignore_ascii_case("localhost") + || host + .parse::() + .map(|address| address.is_loopback()) + .unwrap_or(false) +} + fn request_completion( endpoint: &AiEndpointConfig, payload: &Value, cancelled: &AtomicBool, mut on_content: impl FnMut(&str), ) -> EngineResult { - let client = Client::builder() - .connect_timeout(Duration::from_secs(5)) - .timeout(Duration::from_secs(300)) - .build()?; + let client = chat_http_client(&endpoint.url)?; let mut request = client .post(chat_completions_url(&endpoint.url)) .json(payload); diff --git a/crates/bds-core/src/engine/preview.rs b/crates/bds-core/src/engine/preview.rs index 1ed5ebb..d115949 100644 --- a/crates/bds-core/src/engine/preview.rs +++ b/crates/bds-core/src/engine/preview.rs @@ -664,6 +664,13 @@ mod tests { GUARD.get_or_init(|| Mutex::new(())) } + fn preview_client() -> reqwest::blocking::Client { + reqwest::blocking::Client::builder() + .no_proxy() + .build() + .unwrap() + } + fn setup_preview_fixture() -> (tempfile::TempDir, Database) { let dir = tempfile::tempdir().unwrap(); std::fs::create_dir_all(dir.path().join("meta")).unwrap(); @@ -1112,7 +1119,7 @@ mod tests { ) .unwrap(); - let client = reqwest::blocking::Client::new(); + let client = preview_client(); let mut body = None; for _ in 0..20 { if let Ok(response) = client @@ -1229,7 +1236,7 @@ mod tests { ) .unwrap(); - let client = reqwest::blocking::Client::new(); + let client = preview_client(); let response = client .get(format!( "http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/../outside.txt" @@ -1269,7 +1276,7 @@ mod tests { ) .unwrap(); - let client = reqwest::blocking::Client::new(); + let client = preview_client(); let media = client .get(format!("http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/ok.txt")) .send() @@ -1362,7 +1369,7 @@ mod tests { "project-1".into(), ) .unwrap(); - let client = reqwest::blocking::Client::new(); + let client = preview_client(); let preview_html = client .get(format!( "http://{PREVIEW_HOST}:{PREVIEW_PORT}/__draft/post-1" @@ -1427,7 +1434,7 @@ mod tests { ) .unwrap(); - let client = reqwest::blocking::Client::new(); + let client = preview_client(); let response = client .get(format!( "http://{PREVIEW_HOST}:{PREVIEW_PORT}/__style-preview?theme=amber&mode=dark" @@ -1487,7 +1494,7 @@ mod tests { ) .unwrap(); - let client = reqwest::blocking::Client::new(); + let client = preview_client(); let response = client .get(format!( "http://{PREVIEW_HOST}:{PREVIEW_PORT}/?theme=amber&mode=light" diff --git a/crates/bds-core/tests/mcp.rs b/crates/bds-core/tests/mcp.rs index d7e8455..acf519a 100644 --- a/crates/bds-core/tests/mcp.rs +++ b/crates/bds-core/tests/mcp.rs @@ -671,7 +671,10 @@ fn protocol_routes_every_family_and_http_enforces_loopback_origin_and_cors() { ); let server = mcp::McpHttpServer::start(fixture.database_path.clone(), 0).unwrap(); - let client = reqwest::blocking::Client::new(); + let client = reqwest::blocking::Client::builder() + .no_proxy() + .build() + .unwrap(); let response = client .post(server.endpoint()) .header("accept", "application/json, text/event-stream") diff --git a/crates/bds-ui/src/platform/script_host.rs b/crates/bds-ui/src/platform/script_host.rs index 7eb3aea..98e9588 100644 --- a/crates/bds-ui/src/platform/script_host.rs +++ b/crates/bds-ui/src/platform/script_host.rs @@ -123,8 +123,11 @@ mod tests { #[test] fn desktop_handler_queues_supported_menu_actions() { let queued = Arc::new(Mutex::new(Vec::new())); - handler(Arc::clone(&queued), String::new())("trigger_menu_action", &[serde_json::json!("new_post")]) - .unwrap(); + handler(Arc::clone(&queued), String::new())( + "trigger_menu_action", + &[serde_json::json!("new_post")], + ) + .unwrap(); assert_eq!(queued.lock().unwrap().as_slice(), &[MenuAction::NewPost]); } }