liter_llm/http/
transport.rs1use std::time::Duration;
21
22#[derive(Clone, Debug)]
28pub struct TransportConfig {
29 pub pool_max_idle_per_host: usize,
33
34 pub pool_idle_timeout: Option<Duration>,
38
39 pub tcp_keepalive: Option<Duration>,
44
45 pub http2_prior_knowledge: bool,
53
54 pub dns_cache_ttl: Option<Duration>,
60
61 #[cfg(feature = "http3")]
69 pub enable_http3: bool,
70
71 pub disable_proxies: bool,
84}
85
86impl Default for TransportConfig {
87 fn default() -> Self {
88 Self {
89 pool_max_idle_per_host: 32,
90 pool_idle_timeout: Some(Duration::from_secs(90)),
91 tcp_keepalive: Some(Duration::from_secs(60)),
92 http2_prior_knowledge: false,
93 dns_cache_ttl: Some(Duration::from_secs(30)),
94 #[cfg(feature = "http3")]
95 enable_http3: false,
96 disable_proxies: false,
97 }
98 }
99}
100
101impl TransportConfig {
102 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub fn with_pool_max_idle_per_host(mut self, count: usize) -> Self {
109 self.pool_max_idle_per_host = count;
110 self
111 }
112
113 pub fn with_pool_idle_timeout(mut self, timeout: Option<Duration>) -> Self {
115 self.pool_idle_timeout = timeout;
116 self
117 }
118
119 pub fn with_tcp_keepalive(mut self, interval: Option<Duration>) -> Self {
121 self.tcp_keepalive = interval;
122 self
123 }
124
125 pub fn with_http2_prior_knowledge(mut self, enabled: bool) -> Self {
127 self.http2_prior_knowledge = enabled;
128 self
129 }
130
131 pub fn with_dns_cache_ttl(mut self, ttl: Option<Duration>) -> Self {
133 self.dns_cache_ttl = ttl;
134 self
135 }
136
137 #[cfg(feature = "http3")]
139 pub fn with_http3(mut self, enabled: bool) -> Self {
140 self.enable_http3 = enabled;
141 self
142 }
143
144 pub fn with_proxies_disabled(mut self, disabled: bool) -> Self {
146 self.disable_proxies = disabled;
147 self
148 }
149
150 #[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
179 pub fn apply_to_builder(&self, builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
180 let builder = builder
181 .pool_max_idle_per_host(self.pool_max_idle_per_host)
182 .pool_idle_timeout(self.pool_idle_timeout)
183 .tcp_keepalive(self.tcp_keepalive);
184
185 let builder = if self.http2_prior_knowledge {
186 builder.http2_prior_knowledge()
187 } else {
188 builder
189 };
190
191 let builder = if self.disable_proxies {
192 builder.no_proxy()
193 } else {
194 builder
195 };
196
197 #[cfg(feature = "http3")]
198 let _ = self.enable_http3;
199
200 builder
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_default_config() {
210 let cfg = TransportConfig::default();
211 assert_eq!(cfg.pool_max_idle_per_host, 32);
212 assert_eq!(cfg.pool_idle_timeout, Some(Duration::from_secs(90)));
213 assert_eq!(cfg.tcp_keepalive, Some(Duration::from_secs(60)));
214 assert!(!cfg.http2_prior_knowledge);
215 assert_eq!(cfg.dns_cache_ttl, Some(Duration::from_secs(30)));
216 #[cfg(feature = "http3")]
217 assert!(!cfg.enable_http3);
218 assert!(!cfg.disable_proxies);
219 }
220
221 #[test]
222 fn test_builder_chain() {
223 let cfg = TransportConfig::new()
224 .with_pool_max_idle_per_host(16)
225 .with_pool_idle_timeout(Some(Duration::from_secs(45)))
226 .with_tcp_keepalive(Some(Duration::from_secs(120)))
227 .with_http2_prior_knowledge(true)
228 .with_dns_cache_ttl(Some(Duration::from_secs(60)));
229
230 assert_eq!(cfg.pool_max_idle_per_host, 16);
231 assert_eq!(cfg.pool_idle_timeout, Some(Duration::from_secs(45)));
232 assert_eq!(cfg.tcp_keepalive, Some(Duration::from_secs(120)));
233 assert!(cfg.http2_prior_knowledge);
234 assert_eq!(cfg.dns_cache_ttl, Some(Duration::from_secs(60)));
235 }
236
237 #[test]
238 fn test_disable_pooling() {
239 let cfg = TransportConfig::new().with_pool_max_idle_per_host(0);
240 assert_eq!(cfg.pool_max_idle_per_host, 0);
241 }
242
243 #[test]
244 fn test_disable_keepalive() {
245 let cfg = TransportConfig::new().with_tcp_keepalive(None);
246 assert_eq!(cfg.tcp_keepalive, None);
247 }
248
249 #[test]
250 fn test_disable_dns_cache() {
251 let cfg = TransportConfig::new().with_dns_cache_ttl(None);
252 assert_eq!(cfg.dns_cache_ttl, None);
253 }
254
255 #[cfg(feature = "http3")]
256 #[test]
257 fn test_http3_flag() {
258 let cfg = TransportConfig::new().with_http3(true);
259 assert!(cfg.enable_http3);
260 }
261
262 #[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
267 #[test]
268 fn test_apply_to_builder_builds_client_with_non_default_config() {
269 let cfg = TransportConfig::new()
270 .with_pool_max_idle_per_host(128)
271 .with_pool_idle_timeout(Some(Duration::from_secs(45)))
272 .with_tcp_keepalive(Some(Duration::from_secs(30)))
273 .with_http2_prior_knowledge(false)
274 .with_dns_cache_ttl(Some(Duration::from_secs(60)));
275
276 let builder = reqwest::Client::builder();
277 let builder = cfg.apply_to_builder(builder);
278 let client = builder.build();
279 assert!(
280 client.is_ok(),
281 "reqwest::Client::build() failed with non-default TransportConfig"
282 );
283 }
284
285 #[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
288 #[test]
289 fn test_apply_to_builder_with_http2_prior_knowledge() {
290 let cfg = TransportConfig::new().with_http2_prior_knowledge(true);
291 let client = cfg.apply_to_builder(reqwest::Client::builder()).build();
292 assert!(
293 client.is_ok(),
294 "reqwest::Client::build() failed with http2_prior_knowledge=true"
295 );
296 }
297
298 #[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
300 #[test]
301 fn test_apply_to_builder_with_pooling_disabled() {
302 let cfg = TransportConfig::new()
303 .with_pool_max_idle_per_host(0)
304 .with_pool_idle_timeout(None)
305 .with_tcp_keepalive(None)
306 .with_dns_cache_ttl(None);
307
308 let client = cfg.apply_to_builder(reqwest::Client::builder()).build();
309 assert!(client.is_ok(), "reqwest::Client::build() failed with pooling disabled");
310 }
311
312 #[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
315 #[tokio::test]
316 async fn test_disable_proxies_bypasses_configured_proxy() {
317 use tokio::io::{AsyncReadExt, AsyncWriteExt};
318
319 async fn serve_once(listener: tokio::net::TcpListener) {
320 let (mut socket, _) = listener.accept().await.expect("accept");
321 let mut buf = [0u8; 1024];
322 let _ = socket.read(&mut buf).await;
323 let _ = socket
324 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\norigin")
325 .await;
326 }
327
328 let origin = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind origin");
329 let origin_url = format!("http://{}/", origin.local_addr().expect("origin address"));
330 let proxy = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind proxy");
331 let proxy_url = format!("http://{}", proxy.local_addr().expect("proxy address"));
332 let proxy_hit = tokio::spawn(async move { proxy.accept().await.is_ok() });
333 let origin_task = tokio::spawn(serve_once(origin));
334
335 let builder = reqwest::Client::builder().proxy(reqwest::Proxy::all(&proxy_url).expect("proxy URL"));
336 let client = TransportConfig::new()
337 .with_proxies_disabled(true)
338 .apply_to_builder(builder)
339 .build()
340 .expect("client builds");
341 let body = client
342 .get(&origin_url)
343 .send()
344 .await
345 .expect("request reaches origin")
346 .text()
347 .await
348 .expect("body");
349
350 assert_eq!(body, "origin");
351 origin_task.await.expect("origin served");
352 assert!(!proxy_hit.is_finished(), "the proxy must not be contacted");
353 proxy_hit.abort();
354 }
355}