Skip to main content

turbo_cdn/
http_client.rs

1// Licensed under the MIT License
2// Copyright (c) 2025 Hal <hal.long@outlook.com>
3
4//! HTTP client implementation using reqwest with rustls
5//!
6//! This module provides a simple, reliable HTTP client implementation
7//! using reqwest with rustls for better cross-platform compatibility.
8
9use crate::error::{Result, TurboCdnError};
10use std::collections::HashMap;
11use std::time::Duration;
12
13/// HTTP response abstraction
14#[derive(Debug)]
15pub struct HttpResponse {
16    pub status: u16,
17    pub headers: HashMap<String, String>,
18    pub body: Vec<u8>,
19}
20
21/// HTTP client implementation using reqwest
22#[derive(Debug)]
23pub struct HttpClient {
24    client: reqwest::Client,
25}
26
27impl HttpClient {
28    /// Create a new HTTP client with the specified timeout
29    pub fn new(timeout: Duration) -> Result<Self> {
30        // Initialize rustls provider before creating reqwest client
31        crate::init_rustls_provider();
32
33        let client = reqwest::Client::builder()
34            .timeout(timeout)
35            .tcp_keepalive(Duration::from_secs(60))
36            .tcp_nodelay(true)
37            .pool_max_idle_per_host(20)
38            .http2_prior_knowledge()
39            .build()
40            .map_err(|e| TurboCdnError::network(format!("Failed to create HTTP client: {e}")))?;
41
42        Ok(Self { client })
43    }
44
45    /// Perform a GET request
46    pub async fn get(&self, url: &str) -> Result<HttpResponse> {
47        let response = self
48            .client
49            .get(url)
50            .send()
51            .await
52            .map_err(|e| TurboCdnError::network(format!("GET request failed: {e}")))?;
53
54        let status = response.status().as_u16();
55        let headers = response
56            .headers()
57            .iter()
58            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
59            .collect();
60
61        let body = response
62            .bytes()
63            .await
64            .map_err(|e| TurboCdnError::network(format!("Failed to read response body: {e}")))?
65            .to_vec();
66
67        Ok(HttpResponse {
68            status,
69            headers,
70            body,
71        })
72    }
73
74    /// Perform a GET request with custom headers
75    pub async fn get_with_headers(
76        &self,
77        url: &str,
78        request_headers: &HashMap<String, String>,
79    ) -> Result<HttpResponse> {
80        let mut request = self.client.get(url);
81
82        for (key, value) in request_headers {
83            request = request.header(key, value);
84        }
85
86        let response = request
87            .send()
88            .await
89            .map_err(|e| TurboCdnError::network(format!("GET request with headers failed: {e}")))?;
90
91        let status = response.status().as_u16();
92        let headers = response
93            .headers()
94            .iter()
95            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
96            .collect();
97
98        let body = response
99            .bytes()
100            .await
101            .map_err(|e| TurboCdnError::network(format!("Failed to read response body: {e}")))?
102            .to_vec();
103
104        Ok(HttpResponse {
105            status,
106            headers,
107            body,
108        })
109    }
110
111    /// Perform a HEAD request
112    pub async fn head(&self, url: &str) -> Result<HttpResponse> {
113        let response = self
114            .client
115            .head(url)
116            .send()
117            .await
118            .map_err(|e| TurboCdnError::network(format!("HEAD request failed: {e}")))?;
119
120        let status = response.status().as_u16();
121        let headers = response
122            .headers()
123            .iter()
124            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
125            .collect();
126
127        Ok(HttpResponse {
128            status,
129            headers,
130            body: Vec::new(),
131        })
132    }
133
134    /// Get client name for debugging
135    pub fn name(&self) -> &'static str {
136        "reqwest"
137    }
138}