diff --git a/CHANGELOG.md b/CHANGELOG.md index 101cd4c..aa97373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Secret redaction regex-based pattern matching replaces whitespace tokenizer, detecting secrets in URLs, JSON, and quoted strings - Added `hf_`, `npm_`, `dckr_pat_` to secret redaction prefixes - A2A client stream errors truncate upstream body to 256 bytes +- Add `default_client()` HTTP helper with standard timeouts and user-agent in zeph-core and zeph-llm (#666) +- Replace 5 production `Client::new()` calls with `default_client()` for consistent HTTP config (#667) ### Fixed - False positive: "sudoku" no longer matched by "sudo" blocked pattern (word-boundary matching) diff --git a/Cargo.lock b/Cargo.lock index 2fa4aad..2d5fa22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8936,6 +8936,7 @@ dependencies = [ "notify-debouncer-mini", "proptest", "regex", + "reqwest 0.13.2", "schemars 1.2.1", "serde", "serde_json", diff --git a/crates/zeph-channels/src/discord/rest.rs b/crates/zeph-channels/src/discord/rest.rs index 3646a47..6f13448 100644 --- a/crates/zeph-channels/src/discord/rest.rs +++ b/crates/zeph-channels/src/discord/rest.rs @@ -35,7 +35,7 @@ struct EditMessage<'a> { impl RestClient { #[must_use] pub fn new(token: String) -> Self { - let client = reqwest::Client::new(); + let client = zeph_core::http::default_client(); Self { client, token } } diff --git a/crates/zeph-channels/src/slack/api.rs b/crates/zeph-channels/src/slack/api.rs index 8483d64..9f3dc61 100644 --- a/crates/zeph-channels/src/slack/api.rs +++ b/crates/zeph-channels/src/slack/api.rs @@ -45,7 +45,7 @@ impl SlackApi { #[must_use] pub fn new(token: String) -> Self { Self { - client: reqwest::Client::new(), + client: zeph_core::http::default_client(), token, } } diff --git a/crates/zeph-core/Cargo.toml b/crates/zeph-core/Cargo.toml index 01a0cf7..7e34b24 100644 --- a/crates/zeph-core/Cargo.toml +++ b/crates/zeph-core/Cargo.toml @@ -17,6 +17,7 @@ metal = ["zeph-llm/metal"] [dependencies] age.workspace = true anyhow.workspace = true +reqwest = { workspace = true, features = ["rustls"] } futures.workspace = true notify.workspace = true notify-debouncer-mini.workspace = true diff --git a/crates/zeph-core/src/http.rs b/crates/zeph-core/src/http.rs new file mode 100644 index 0000000..6d3f0f1 --- /dev/null +++ b/crates/zeph-core/src/http.rs @@ -0,0 +1,32 @@ +//! Shared HTTP client construction for consistent timeout and TLS configuration. + +use std::time::Duration; + +/// Create a shared HTTP client with standard Zeph configuration. +/// +/// Config: 30s connect timeout, 60s request timeout, rustls TLS, +/// `zeph/{version}` user-agent, redirect limit 10. +/// +/// # Panics +/// +/// Panics if the TLS backend cannot be initialized (should never happen with rustls). +#[must_use] +pub fn default_client() -> reqwest::Client { + reqwest::Client::builder() + .connect_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(60)) + .user_agent(concat!("zeph/", env!("CARGO_PKG_VERSION"))) + .redirect(reqwest::redirect::Policy::limited(10)) + .build() + .expect("default HTTP client construction must not fail") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn client_builds_successfully() { + let _client = default_client(); + } +} diff --git a/crates/zeph-core/src/lib.rs b/crates/zeph-core/src/lib.rs index 4de7fe7..6ec9e40 100644 --- a/crates/zeph-core/src/lib.rs +++ b/crates/zeph-core/src/lib.rs @@ -17,6 +17,7 @@ pub mod redact; pub mod vault; pub mod diff; +pub mod http; pub use agent::Agent; pub use agent::error::AgentError; diff --git a/crates/zeph-llm/src/claude.rs b/crates/zeph-llm/src/claude.rs index 5fa0916..93889e2 100644 --- a/crates/zeph-llm/src/claude.rs +++ b/crates/zeph-llm/src/claude.rs @@ -59,7 +59,7 @@ impl ClaudeProvider { #[must_use] pub fn new(api_key: String, model: String, max_tokens: u32) -> Self { Self { - client: reqwest::Client::new(), + client: crate::http::default_client(), api_key, model, max_tokens, diff --git a/crates/zeph-llm/src/http.rs b/crates/zeph-llm/src/http.rs new file mode 100644 index 0000000..81a9b28 --- /dev/null +++ b/crates/zeph-llm/src/http.rs @@ -0,0 +1,18 @@ +//! Shared HTTP client construction for consistent timeout and TLS configuration. + +use std::time::Duration; + +/// Create a shared HTTP client with standard Zeph configuration. +/// +/// Config: 30s connect timeout, 60s request timeout, rustls TLS, +/// `zeph/{version}` user-agent, redirect limit 10. +#[must_use] +pub fn default_client() -> reqwest::Client { + reqwest::Client::builder() + .connect_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(60)) + .user_agent(concat!("zeph/", env!("CARGO_PKG_VERSION"))) + .redirect(reqwest::redirect::Policy::limited(10)) + .build() + .expect("default HTTP client construction must not fail") +} diff --git a/crates/zeph-llm/src/lib.rs b/crates/zeph-llm/src/lib.rs index d84c5b2..5532961 100644 --- a/crates/zeph-llm/src/lib.rs +++ b/crates/zeph-llm/src/lib.rs @@ -9,6 +9,7 @@ pub mod claude; pub mod compatible; pub mod error; pub mod extractor; +pub(crate) mod http; #[cfg(feature = "mock")] pub mod mock; pub mod ollama; diff --git a/crates/zeph-llm/src/openai.rs b/crates/zeph-llm/src/openai.rs index 9189b59..436226c 100644 --- a/crates/zeph-llm/src/openai.rs +++ b/crates/zeph-llm/src/openai.rs @@ -69,7 +69,7 @@ impl OpenAiProvider { base_url.pop(); } Self { - client: reqwest::Client::new(), + client: crate::http::default_client(), api_key, base_url, model, diff --git a/src/main.rs b/src/main.rs index e374c0f..2f55f84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -551,7 +551,7 @@ async fn main() -> anyhow::Result<()> { .as_ref() .map_or(String::new(), |k| k.expose().to_string()); let whisper = zeph_llm::whisper::WhisperProvider::new( - reqwest::Client::new(), + zeph_core::http::default_client(), api_key, base_url, &stt_cfg.model,