lescuer97 3 settimane fa
parent
commit
0e3c59bce8

+ 1 - 1
crates/cdk-http-client/Cargo.toml

@@ -12,7 +12,7 @@ readme = "README.md"
 
 [features]
 default = ["bitreq"]
-reqwest = ["dep:reqwest"]
+reqwest = ["dep:reqwest", "dep:regex"]
 bitreq = ["dep:bitreq", "dep:serde_urlencoded", "dep:regex"]
 
 [dependencies]

+ 47 - 7
crates/cdk-http-client/src/backends/bitreq_backend.rs

@@ -2,22 +2,48 @@
 
 use serde::de::DeserializeOwned;
 use serde::Serialize;
+use std::sync::Arc;
 
+use bitreq::RequestExt;
+
+use crate::client::{apply_proxy_if_needed, ProxyConfig};
 use crate::error::HttpError;
 use crate::request_builder_ext::RequestBuilderExt;
 use crate::response::{RawResponse, Response};
 
 /// bitreq-based RequestBuilder wrapper
-#[derive(Debug)]
 pub struct BitreqRequestBuilder {
     inner: bitreq::Request,
     error: Option<HttpError>,
+    url: String,
+    client: Arc<bitreq::Client>,
+    proxy_config: Option<ProxyConfig>,
+}
+
+impl std::fmt::Debug for BitreqRequestBuilder {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("BitreqRequestBuilder")
+            .field("url", &self.url)
+            .field("error", &self.error)
+            .finish_non_exhaustive()
+    }
 }
 
 impl BitreqRequestBuilder {
     /// Create a new BitreqRequestBuilder from a bitreq::Request
-    pub(crate) fn new(inner: bitreq::Request) -> Self {
-        Self { inner, error: None }
+    pub(crate) fn new(
+        inner: bitreq::Request,
+        url: &str,
+        client: Arc<bitreq::Client>,
+        proxy_config: Option<ProxyConfig>,
+    ) -> Self {
+        Self {
+            inner,
+            error: None,
+            url: url.to_string(),
+            client,
+            proxy_config,
+        }
     }
 }
 
@@ -25,7 +51,10 @@ impl RequestBuilderExt for BitreqRequestBuilder {
     fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
         Self {
             inner: self.inner.with_header(key.as_ref(), value.as_ref()),
-            error: None,
+            error: self.error,
+            url: self.url,
+            client: self.client,
+            proxy_config: self.proxy_config,
         }
     }
 
@@ -43,7 +72,10 @@ impl RequestBuilderExt for BitreqRequestBuilder {
     fn form<T: Serialize>(mut self, body: &T) -> Self {
         match serde_urlencoded::to_string(body) {
             Ok(form_str) => {
-                self.inner = self.inner.with_body(form_str.into_bytes());
+                self.inner = self
+                    .inner
+                    .with_body(form_str.into_bytes())
+                    .with_header("Content-Type", "application/x-www-form-urlencoded");
             }
             Err(e) => self.error = Some(HttpError::Serialization(e.to_string())),
         }
@@ -54,7 +86,11 @@ impl RequestBuilderExt for BitreqRequestBuilder {
         if let Some(err) = self.error {
             return Err(err);
         }
-        let response = self.inner.send_async().await.map_err(HttpError::from)?;
+        let request = apply_proxy_if_needed(self.inner, &self.url, &self.proxy_config)?;
+        let response = request
+            .send_async_with_client(&self.client)
+            .await
+            .map_err(HttpError::from)?;
         Ok(RawResponse::new(response))
     }
 
@@ -62,7 +98,11 @@ impl RequestBuilderExt for BitreqRequestBuilder {
         if let Some(err) = self.error {
             return Err(err);
         }
-        let response = self.inner.send_async().await.map_err(HttpError::from)?;
+        let request = apply_proxy_if_needed(self.inner, &self.url, &self.proxy_config)?;
+        let response = request
+            .send_async_with_client(&self.client)
+            .await
+            .map_err(HttpError::from)?;
         let status = response.status_code;
 
         if !(200..300).contains(&status) {

+ 100 - 34
crates/cdk-http-client/src/client.rs

@@ -2,6 +2,8 @@
 
 #[cfg(feature = "bitreq")]
 use bitreq::RequestExt;
+#[cfg(feature = "bitreq")]
+use std::sync::Arc;
 use serde::de::DeserializeOwned;
 use serde::Serialize;
 
@@ -15,7 +17,7 @@ pub struct HttpClient {
     #[cfg(feature = "reqwest")]
     inner: reqwest::Client,
     #[cfg(feature = "bitreq")]
-    inner: bitreq::Client,
+    inner: Arc<bitreq::Client>,
     #[cfg(feature = "bitreq")]
     proxy_config: Option<ProxyConfig>,
 }
@@ -166,7 +168,7 @@ impl HttpClient {
     /// Create a new HTTP client with default settings
     pub fn new() -> Self {
         Self {
-            inner: bitreq::Client::new(10), // Default capacity of 10
+            inner: Arc::new(bitreq::Client::new(10)), // Default capacity of 10
             proxy_config: None,
         }
     }
@@ -182,26 +184,7 @@ impl HttpClient {
         request: bitreq::Request,
         url: &str,
     ) -> Response<bitreq::Request> {
-        if let Some(ref config) = self.proxy_config {
-            if let Some(ref matcher) = config.matcher {
-                // Check if URL host matches the regex pattern
-                if let Ok(parsed_url) = url::Url::parse(url) {
-                    if let Some(host) = parsed_url.host_str() {
-                        if matcher.is_match(host) {
-                            let proxy = bitreq::Proxy::new_http(&config.url.to_string())
-                                .map_err(|e| HttpError::Proxy(e.to_string()))?;
-                            return Ok(request.with_proxy(proxy));
-                        }
-                    }
-                }
-            } else {
-                // No matcher, apply proxy to all requests
-                let proxy = bitreq::Proxy::new_http(&config.url.to_string())
-                    .map_err(|e| HttpError::Proxy(e.to_string()))?;
-                return Ok(request.with_proxy(proxy));
-            }
-        }
-        Ok(request)
+        apply_proxy_if_needed(request, url, &self.proxy_config)
     }
 
     /// GET request, returns JSON deserialized to R
@@ -258,7 +241,9 @@ impl HttpClient {
     ) -> Response<R> {
         let form_str = serde_urlencoded::to_string(form)
             .map_err(|e| HttpError::Serialization(e.to_string()))?;
-        let request = bitreq::post(url).with_body(form_str.into_bytes());
+        let request = bitreq::post(url)
+            .with_body(form_str.into_bytes())
+            .with_header("Content-Type", "application/x-www-form-urlencoded");
         let request = self.apply_proxy_if_needed(request, url)?;
         let response: bitreq::Response = request
             .send_async_with_client(&self.inner)
@@ -318,17 +303,32 @@ impl HttpClient {
     /// POST request builder for complex cases
     pub fn post(&self, url: &str) -> RequestBuilder {
         // Note: Proxy will be applied when the request is sent
-        RequestBuilder::new(bitreq::post(url))
+        RequestBuilder::new(
+            bitreq::post(url),
+            url,
+            self.inner.clone(),
+            self.proxy_config.clone(),
+        )
     }
 
     /// GET request builder for complex cases
     pub fn get(&self, url: &str) -> RequestBuilder {
-        RequestBuilder::new(bitreq::get(url))
+        RequestBuilder::new(
+            bitreq::get(url),
+            url,
+            self.inner.clone(),
+            self.proxy_config.clone(),
+        )
     }
 
     /// PATCH request builder for complex cases
     pub fn patch(&self, url: &str) -> RequestBuilder {
-        RequestBuilder::new(bitreq::patch(url))
+        RequestBuilder::new(
+            bitreq::patch(url),
+            url,
+            self.inner.clone(),
+            self.proxy_config.clone(),
+        )
     }
 }
 
@@ -341,20 +341,67 @@ impl Default for HttpClient {
 /// HTTP client builder for configuring proxy and TLS settings
 #[derive(Debug, Default)]
 pub struct HttpClientBuilder {
+    #[cfg(any(feature = "reqwest", feature = "bitreq"))]
+    proxy: Option<ProxyConfig>,
     #[cfg(feature = "bitreq")]
     accept_invalid_certs: bool,
-    #[cfg(feature = "bitreq")]
-    proxy: Option<ProxyConfig>,
 }
 
-#[cfg(feature = "bitreq")]
+#[cfg(any(feature = "bitreq", feature = "reqwest"))]
 #[derive(Debug, Clone)]
-struct ProxyConfig {
+pub(crate) struct ProxyConfig {
     url: url::Url,
     matcher: Option<regex::Regex>,
 }
 
+#[cfg(feature = "bitreq")]
+pub(crate) fn apply_proxy_if_needed(
+    request: bitreq::Request,
+    url: &str,
+    proxy_config: &Option<ProxyConfig>,
+) -> Response<bitreq::Request> {
+    if let Some(ref config) = proxy_config {
+        if let Some(ref matcher) = config.matcher {
+            // Check if URL host matches the regex pattern
+            if let Ok(parsed_url) = url::Url::parse(url) {
+                if let Some(host) = parsed_url.host_str() {
+                    if matcher.is_match(host) {
+                        let proxy = bitreq::Proxy::new_http(&config.url)
+                            .map_err(|e| HttpError::Proxy(e.to_string()))?;
+                        return Ok(request.with_proxy(proxy));
+                    }
+                }
+            }
+        } else {
+            // No matcher, apply proxy to all requests
+            let proxy = bitreq::Proxy::new_http(&config.url)
+                .map_err(|e| HttpError::Proxy(e.to_string()))?;
+            return Ok(request.with_proxy(proxy));
+        }
+    }
+    Ok(request)
+}
+
 impl HttpClientBuilder {
+    /// Set a proxy URL (reqwest only)
+    #[cfg(feature = "reqwest")]
+    pub fn proxy(mut self, url: url::Url) -> Self {
+        self.proxy = Some(ProxyConfig { url, matcher: None });
+        self
+    }
+
+    /// Set a proxy URL with a host pattern matcher (reqwest only)
+    #[cfg(feature = "reqwest")]
+    pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
+        let matcher = regex::Regex::new(pattern)
+            .map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
+        self.proxy = Some(ProxyConfig {
+            url,
+            matcher: Some(matcher),
+        });
+        Ok(self)
+    }
+
     /// Accept invalid TLS certificates (bitreq only)
     #[cfg(feature = "bitreq")]
     pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
@@ -385,9 +432,28 @@ impl HttpClientBuilder {
     pub fn build(self) -> Response<HttpClient> {
         #[cfg(feature = "reqwest")]
         {
-            Ok(HttpClient {
-                inner: reqwest::Client::new(),
-            })
+            let mut builder = reqwest::Client::builder();
+            if let Some(proxy) = self.proxy {
+                let proxy_url = proxy.url;
+                if let Some(matcher) = proxy.matcher {
+                    let custom_proxy = reqwest::Proxy::custom(move |url| {
+                        url.host_str().and_then(|host| {
+                            if matcher.is_match(host) {
+                                Some(proxy_url.clone())
+                            } else {
+                                None
+                            }
+                        })
+                    });
+                    builder = builder.proxy(custom_proxy);
+                } else {
+                    let proxy = reqwest::Proxy::all(proxy_url)
+                        .map_err(|e| HttpError::Proxy(e.to_string()))?;
+                    builder = builder.proxy(proxy);
+                }
+            }
+            let client = builder.build().map_err(|e| HttpError::Build(e.to_string()))?;
+            Ok(HttpClient { inner: client })
         }
 
         #[cfg(feature = "bitreq")]
@@ -400,7 +466,7 @@ impl HttpClientBuilder {
             }
 
             Ok(HttpClient {
-                inner: bitreq::Client::new(10), // Default capacity of 10
+                inner: Arc::new(bitreq::Client::new(10)), // Default capacity of 10
                 proxy_config: self.proxy,
             })
         }

+ 7 - 6
crates/cdk-http-client/src/request_builder_ext.rs

@@ -20,12 +20,13 @@ pub trait RequestBuilderExt: Sized + Send {
     fn form<T: Serialize>(self, body: &T) -> Self;
 
     /// Send the request and return a raw response
-    async fn send(self) -> Response<RawResponse>;
+    fn send(self) -> impl std::future::Future<Output = Response<RawResponse>> + Send;
 
     /// Send the request and deserialize the response as JSON
-    async fn send_json<R: DeserializeOwned>(self) -> Response<R>;
+    fn send_json<R: DeserializeOwned>(self) -> impl std::future::Future<Output = Response<R>> + Send;
 }
 
+#[allow(clippy::manual_async_fn)]
 impl<T: RequestBuilderExt> RequestBuilderExt for Box<T> {
     fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
         Box::new((*self).header(key, value))
@@ -39,11 +40,11 @@ impl<T: RequestBuilderExt> RequestBuilderExt for Box<T> {
         Box::new((*self).form(body))
     }
 
-    async fn send(self) -> Response<RawResponse> {
-        (*self).send().await
+    fn send(self) -> impl std::future::Future<Output = Response<RawResponse>> + Send {
+        async move { (*self).send().await }
     }
 
-    async fn send_json<R: DeserializeOwned>(self) -> Response<R> {
-        (*self).send_json().await
+    fn send_json<R: DeserializeOwned>(self) -> impl std::future::Future<Output = Response<R>> + Send {
+        async move { (*self).send_json().await }
     }
 }