Skip to main content

baidu_netdisk_sdk/http/
client.rs

1//! HTTP client implementation
2//!
3//! Provides robust HTTP request handling with retry, timeout, and error handling
4use std::time::Duration;
5
6use log::{debug, error, warn};
7use reqwest::{Client, ClientBuilder, Response, Url};
8use serde::{de::DeserializeOwned, Serialize};
9
10use crate::auth::{ApiErrorResponse, AuthErrorResponse};
11use crate::errors::{NetDiskError, NetDiskResult};
12
13/// HTTP client configuration
14///
15/// Customizes HTTP client behavior
16#[derive(Debug, Clone)]
17pub struct HttpClientConfig {
18    /// Request timeout duration
19    pub timeout: Duration,
20    /// Connection timeout duration
21    pub connect_timeout: Duration,
22    /// Maximum number of retry attempts
23    pub max_retries: usize,
24    /// Retry delay in milliseconds
25    pub retry_delay_ms: u64,
26    /// User-Agent string
27    pub user_agent: String,
28    /// Whether to follow redirects
29    pub follow_redirects: bool,
30    /// Maximum number of redirects
31    pub max_redirects: usize,
32}
33
34impl Default for HttpClientConfig {
35    fn default() -> Self {
36        HttpClientConfig {
37            timeout: Duration::from_secs(30),
38            connect_timeout: Duration::from_secs(10),
39            max_retries: 3,
40            retry_delay_ms: 1000,
41            user_agent: "pan.baidu.com".to_string(),
42            follow_redirects: true,
43            max_redirects: 10,
44        }
45    }
46}
47
48/// HTTP client for making API requests
49///
50/// Provides GET, POST, POST form, POST JSON, and multipart file upload methods
51/// with automatic retry and error handling
52#[derive(Debug, Clone)]
53pub struct HttpClient {
54    inner: Client,
55    config: HttpClientConfig,
56    base_url: Url,
57}
58
59impl HttpClient {
60    /// Create a new HTTP client with the given configuration
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use baidu_netdisk_sdk::http::{HttpClient, HttpClientConfig};
66    ///
67    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
68    /// let config = HttpClientConfig::default();
69    /// let client = HttpClient::new(config)?;
70    /// # Ok(())
71    /// # }
72    /// ```
73    pub fn new(config: HttpClientConfig) -> NetDiskResult<Self> {
74        let redirect_policy = if config.follow_redirects {
75            reqwest::redirect::Policy::limited(config.max_redirects)
76        } else {
77            reqwest::redirect::Policy::none()
78        };
79
80        let client = ClientBuilder::new()
81            .timeout(config.timeout)
82            .connect_timeout(config.connect_timeout)
83            .redirect(redirect_policy)
84            .user_agent("pan.baidu.com")
85            .build()
86            .map_err(|e| NetDiskError::Unknown {
87                message: format!("Failed to build HTTP client: {}", e),
88            })?;
89
90        let base_url = Url::parse("https://pan.baidu.com").map_err(|e| NetDiskError::Unknown {
91            message: format!("Failed to parse base URL: {}", e),
92        })?;
93
94        Ok(HttpClient {
95            inner: client,
96            config,
97            base_url,
98        })
99    }
100
101    /// Create a default HTTP client
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use baidu_netdisk_sdk::http::HttpClient;
107    ///
108    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
109    /// let client = HttpClient::try_default()?;
110    /// # Ok(())
111    /// # }
112    /// ```
113    pub fn try_default() -> NetDiskResult<Self> {
114        Self::new(HttpClientConfig::default())
115    }
116
117    /// Send GET request and parse JSON response
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use baidu_netdisk_sdk::http::HttpClient;
123    /// use serde::Deserialize;
124    ///
125    /// #[derive(Deserialize)]
126    /// struct Response {
127    ///     // fields
128    /// }
129    ///
130    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
131    /// let client = HttpClient::try_default()?;
132    /// let response: Response = client.get("https://example.com/api", None).await?;
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub async fn get<T: DeserializeOwned>(
137        &self,
138        url: &str,
139        params: Option<&[(&str, &str)]>,
140    ) -> NetDiskResult<T> {
141        self.get_with_headers(url, params, None).await
142    }
143
144    /// Send GET request with custom headers and parse JSON response
145    pub async fn get_with_headers<T: DeserializeOwned>(
146        &self,
147        url: &str,
148        params: Option<&[(&str, &str)]>,
149        headers: Option<&[(&str, &str)]>,
150    ) -> NetDiskResult<T> {
151        let url = if url.starts_with("http") {
152            Url::parse(url)?
153        } else {
154            self.build_url(url, params.unwrap_or(&[]))?
155        };
156
157        debug!("HTTP GET: {}", url);
158        if let Some(p) = params {
159            if !p.is_empty() {
160                debug!("  Query params: {:?}", p);
161            }
162        }
163        if let Some(h) = headers {
164            if !h.is_empty() {
165                debug!("  Headers: {:?}", h);
166            }
167        }
168
169        self.execute_request_with_retry(|| async {
170            let mut request = self.inner.get(url.clone());
171            if let Some(h) = headers {
172                for (key, value) in h.iter() {
173                    request = request.header(*key, *value);
174                }
175            }
176            request.send().await
177        })
178        .await
179    }
180
181    /// Send POST form request and parse JSON response
182    pub async fn post_form<T: DeserializeOwned>(
183        &self,
184        url: &str,
185        form: Option<&[(&str, &str)]>,
186        params: Option<&[(&str, &str)]>,
187    ) -> NetDiskResult<T> {
188        let url = if url.starts_with("http") {
189            Url::parse(url)?
190        } else {
191            self.build_url(url, params.unwrap_or(&[]))?
192        };
193
194        let form = form.unwrap_or(&[]);
195        debug!("HTTP POST Form: {}", url);
196        if !form.is_empty() {
197            debug!("  Form data: {:?}", form);
198        }
199
200        self.execute_request_with_retry(|| async {
201            self.inner.post(url.clone()).form(form).send().await
202        })
203        .await
204    }
205
206    /// Send POST request with query parameters and parse JSON response
207    pub async fn post<T: DeserializeOwned>(
208        &self,
209        url: &str,
210        params: Option<&[(&str, &str)]>,
211    ) -> NetDiskResult<T> {
212        let url = if url.starts_with("http") {
213            Url::parse(url)?
214        } else {
215            self.build_url(url, params.unwrap_or(&[]))?
216        };
217
218        debug!("HTTP POST: {}", url);
219        if let Some(p) = params {
220            if !p.is_empty() {
221                debug!("  Query params: {:?}", p);
222            }
223        }
224
225        self.execute_request_with_retry(|| async { self.inner.post(url.clone()).send().await })
226            .await
227    }
228
229    /// Send POST JSON request and parse JSON response
230    pub async fn post_json<T: DeserializeOwned, U: Serialize + ?Sized>(
231        &self,
232        url: &str,
233        body: &U,
234    ) -> NetDiskResult<T> {
235        let url = if url.starts_with("http") {
236            Url::parse(url)?
237        } else {
238            self.build_url(url, &[])?
239        };
240
241        let json_body =
242            serde_json::to_string(body).unwrap_or_else(|_| "serialization failed".to_string());
243        debug!("HTTP POST JSON: {}", url);
244        debug!("  Body: {}", json_body);
245
246        self.execute_request_with_retry(|| async {
247            self.inner.post(url.clone()).json(body).send().await
248        })
249        .await
250    }
251
252    /// Send multipart file POST request and parse JSON response
253    pub async fn post_multipart<T: DeserializeOwned>(
254        &self,
255        url: &str,
256        field_name: String,
257        file_name: String,
258        data: Vec<u8>,
259    ) -> NetDiskResult<T> {
260        let url = Url::parse(url)?;
261        debug!("HTTP POST Multipart: {}", url);
262        debug!(
263            "  Field: {}, File: {}, Size: {} bytes",
264            field_name,
265            file_name,
266            data.len()
267        );
268
269        self.execute_request_with_retry(|| async {
270            let form = reqwest::multipart::Form::new().part(
271                field_name.clone(),
272                reqwest::multipart::Part::bytes(data.clone()).file_name(file_name.clone()),
273            );
274            self.inner.post(url.clone()).multipart(form).send().await
275        })
276        .await
277    }
278
279    /// Build URL with path and query parameters
280    fn build_url(&self, path: &str, params: &[(&str, &str)]) -> NetDiskResult<Url> {
281        let mut url = self.base_url.join(path)?;
282
283        if !params.is_empty() {
284            let mut pairs = url.query_pairs_mut();
285            for (key, value) in params {
286                pairs.append_pair(key, value);
287            }
288        }
289
290        debug!("Built URL: {}", url);
291        Ok(url)
292    }
293
294    /// Execute request with retry logic and parse JSON response
295    async fn execute_request_with_retry<T: DeserializeOwned, F, Fut>(
296        &self,
297        make_request: F,
298    ) -> NetDiskResult<T>
299    where
300        F: Fn() -> Fut,
301        Fut: std::future::Future<Output = Result<Response, reqwest::Error>>,
302    {
303        let mut attempts = 0;
304
305        loop {
306            attempts += 1;
307
308            match make_request().await {
309                Ok(response) => {
310                    if response.status().is_server_error() && attempts < self.config.max_retries {
311                        warn!(
312                            "Server error ({}), attempt {}/{}, retrying...",
313                            response.status(),
314                            attempts,
315                            self.config.max_retries
316                        );
317                        tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
318                        continue;
319                    }
320                    return self.parse_response(response).await;
321                }
322                Err(e) => {
323                    if attempts < self.config.max_retries && self.should_retry(&e) {
324                        warn!(
325                            "Request failed, attempt {}/{}, retrying...: {}",
326                            attempts, self.config.max_retries, e
327                        );
328                        tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
329                        continue;
330                    }
331                    return Err(NetDiskError::http_error_with_source(0, "unknown", e));
332                }
333            }
334        }
335    }
336
337    /// Determine if the request should be retried
338    fn should_retry(&self, err: &reqwest::Error) -> bool {
339        err.is_timeout() || err.is_connect() || err.is_body()
340    }
341
342    /// Parse HTTP response
343    async fn parse_response<T: DeserializeOwned>(&self, response: Response) -> NetDiskResult<T> {
344        let status = response.status();
345        let url = response.url().to_string();
346
347        if status.is_success() {
348            let body = response.text().await.map_err(|e| NetDiskError::Unknown {
349                message: format!("Failed to read response body: {}", e),
350            })?;
351
352            debug!("Response body (status {}): {}", status, body);
353
354            match serde_json::from_str(&body) {
355                Ok(data) => Ok(data),
356                Err(e) => {
357                    error!("Failed to parse JSON response: {}", e);
358                    error!("Response body that failed to parse: {}", body);
359                    Err(NetDiskError::Unknown {
360                        message: format!("Failed to parse JSON: {}", e),
361                    })
362                }
363            }
364        } else {
365            let body = match response.text().await {
366                Ok(b) => b,
367                Err(_) => String::from("Unknown error"),
368            };
369
370            error!("API request failed: {} - {}", status, body);
371
372            if let Ok(api_error) = serde_json::from_str::<ApiErrorResponse>(&body) {
373                Err(NetDiskError::api_error(
374                    api_error.get_errno(),
375                    api_error.get_errmsg(),
376                ))
377            } else if let Ok(auth_error) = serde_json::from_str::<AuthErrorResponse>(&body) {
378                Err(NetDiskError::auth_error(&auth_error.error_description))
379            } else {
380                Err(NetDiskError::http_error(status.as_u16(), &url))
381            }
382        }
383    }
384}