Skip to main content

vtcode_commons/
http.rs

1//! HTTP client utilities
2
3use reqwest::{Client, ClientBuilder};
4use std::time::Duration;
5
6pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
7pub const SHORT_TIMEOUT: Duration = Duration::from_secs(5);
8pub const LONG_TIMEOUT: Duration = Duration::from_secs(300);
9
10fn apply_platform_proxy_policy(builder: ClientBuilder) -> ClientBuilder {
11    #[cfg(target_os = "macos")]
12    {
13        // Avoid system proxy discovery on macOS because it can panic in restricted environments.
14        builder.no_proxy()
15    }
16    #[cfg(not(target_os = "macos"))]
17    {
18        builder
19    }
20}
21
22/// Try to build an HTTP client, returning an error if both primary and fallback builders fail.
23///
24/// This is the fallible version of `build_client`. Use this when you want to handle
25/// the error gracefully (e.g., return an error to the caller) instead of panicking.
26pub fn try_build_client<F>(configure: F) -> Result<Client, reqwest::Error>
27where
28    F: Fn(ClientBuilder) -> ClientBuilder,
29{
30    let primary_builder = configure(apply_platform_proxy_policy(ClientBuilder::new()));
31    match primary_builder.build() {
32        Ok(client) => Ok(client),
33        Err(primary_err) => {
34            let fallback_builder = apply_platform_proxy_policy(ClientBuilder::new())
35                .timeout(DEFAULT_TIMEOUT)
36                .connect_timeout(SHORT_TIMEOUT);
37            match fallback_builder.build() {
38                Ok(client) => Ok(client),
39                Err(fallback_err) => {
40                    tracing::error!(
41                        primary_error = %primary_err,
42                        fallback_error = %fallback_err,
43                        "HTTP client creation failed with both primary and fallback configurations"
44                    );
45                    Err(fallback_err)
46                }
47            }
48        }
49    }
50}
51
52/// Create a default HTTP client with standard timeouts.
53///
54/// # Panics
55///
56/// Panics if the HTTP client cannot be created (e.g., TLS library failure).
57/// For a non-panicking version, use [`try_build_client`].
58#[allow(clippy::panic)]
59fn build_client<F>(configure: F) -> Client
60where
61    F: Fn(ClientBuilder) -> ClientBuilder,
62{
63    try_build_client(configure).unwrap_or_else(|e| {
64        panic!(
65            "failed to build HTTP client: {e}. \
66             This usually indicates a TLS configuration issue or system resource exhaustion. \
67             Ensure the system has valid TLS certificates and sufficient resources."
68        )
69    })
70}
71
72/// Create a default HTTP client with standard timeouts
73pub fn create_default_client() -> Client {
74    create_client_with_timeout(DEFAULT_TIMEOUT)
75}
76
77/// Create an HTTP client with a custom timeout
78pub fn create_client_with_timeout(timeout: Duration) -> Client {
79    build_client(|builder| builder.timeout(timeout).connect_timeout(SHORT_TIMEOUT))
80}
81
82/// Create an HTTP client with custom connect and request timeouts
83pub fn create_client_with_timeouts(connect_timeout: Duration, request_timeout: Duration) -> Client {
84    build_client(|builder| {
85        builder
86            .timeout(request_timeout)
87            .connect_timeout(connect_timeout)
88    })
89}
90
91/// Create an HTTP client with a specific user agent
92pub fn create_client_with_user_agent(user_agent: &str) -> Client {
93    build_client(|builder| builder.user_agent(user_agent).timeout(DEFAULT_TIMEOUT))
94}
95
96/// Create an HTTP client optimized for streaming
97pub fn create_streaming_client() -> Client {
98    build_client(|builder| {
99        builder
100            .connect_timeout(SHORT_TIMEOUT)
101            .tcp_keepalive(Some(Duration::from_secs(60)))
102    })
103}
104
105/// Get a default client or create one
106pub fn get_or_create_default_client(existing: Option<Client>) -> Client {
107    existing.unwrap_or_else(create_default_client)
108}