Просмотр исходного кода

Replace reqwest WASM backend with native fetch via web_sys

Remove the reqwest dependency from cdk-http-client and replace it with a native
WASM fetch backend using wasm_bindgen + web_sys. The bitreq backend remains for
native targets but is no longer behind a feature flag — it is always compiled
on non-WASM via target_arch cfg.

- Add wasm_backend.rs: WasmRequestBuilder, HttpClient, and HttpClientBuilder
  using js_fetch + web_sys::Request
- Move HttpClient and HttpClientBuilder into their respective backend files to
  eliminate cfg gates on shared types
- Remove reqwest_backend.rs and the reqwest/bitreq feature flags
- Use cfg(target_arch = "wasm32") / cfg(not(...)) instead of features
- Remove reqwest from workspace dependencies
- Update cdk, cdk-common, cdk-prometheus Cargo.toml references
Cesar Rodas 20 часов назад
Родитель
Сommit
ac0fab4a0d

+ 4 - 2
Cargo.lock

@@ -1405,15 +1405,18 @@ name = "cdk-http-client"
 version = "0.15.0"
 version = "0.15.0"
 dependencies = [
 dependencies = [
  "bitreq",
  "bitreq",
+ "js-sys",
  "mockito",
  "mockito",
  "regex",
  "regex",
- "reqwest",
  "serde",
  "serde",
  "serde_json",
  "serde_json",
  "serde_urlencoded",
  "serde_urlencoded",
  "thiserror 2.0.18",
  "thiserror 2.0.18",
  "tokio",
  "tokio",
  "url",
  "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
 ]
 ]
 
 
 [[package]]
 [[package]]
@@ -1679,7 +1682,6 @@ dependencies = [
  "futures",
  "futures",
  "once_cell",
  "once_cell",
  "prometheus",
  "prometheus",
- "reqwest",
  "serde",
  "serde",
  "serde_json",
  "serde_json",
  "sysinfo",
  "sysinfo",

+ 1 - 10
Cargo.toml

@@ -95,16 +95,7 @@ utoipa = { version = "5.3.1", features = [
     "preserve_path_order",
     "preserve_path_order",
 ]}
 ]}
 serde_with = "3"
 serde_with = "3"
-reqwest = { version = "0.12", default-features = false, features = [
-    "json",
-    "rustls-tls",
-    "rustls-tls-native-roots",
-    "socks",
-    "zstd",
-    "brotli",
-    "gzip",
-    "deflate",
-]}
+
 once_cell = "1.20.2"
 once_cell = "1.20.2"
 web-time = "1.1.0"
 web-time = "1.1.0"
 rand = "0.9.1"
 rand = "0.9.1"

+ 1 - 2
crates/cdk-common/Cargo.toml

@@ -45,13 +45,12 @@ web-time.workspace = true
 parking_lot = "0.12.5"
 parking_lot = "0.12.5"
 paste = "1.0.15"
 paste = "1.0.15"
 tonic = { workspace = true, optional = true }
 tonic = { workspace = true, optional = true }
+cdk-http-client = { workspace = true, optional = true }
 
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-cdk-http-client = { workspace = true, optional = true, features = ["bitreq"] }
 tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "test-util", "sync"] }
 tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "test-util", "sync"] }
 
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-cdk-http-client = { workspace = true, optional = true, default-features = false, features = ["reqwest"] }
 uuid = { workspace = true, features = ["js"], optional = true }
 uuid = { workspace = true, features = ["js"], optional = true }
 getrandom = { version = "0.2", features = ["js"] }
 getrandom = { version = "0.2", features = ["js"] }
 tokio = { workspace = true, default-features = false }
 tokio = { workspace = true, default-features = false }

+ 11 - 14
crates/cdk-http-client/Cargo.toml

@@ -10,23 +10,23 @@ rust-version.workspace = true
 license.workspace = true
 license.workspace = true
 readme = "README.md"
 readme = "README.md"
 
 
-[features]
-# Backend features are mutually exclusive; enable only one.
-default = ["bitreq"]
-reqwest = ["dep:reqwest", "dep:regex"]
-bitreq = ["dep:bitreq", "dep:serde_urlencoded", "dep:regex"]
-
 [dependencies]
 [dependencies]
 serde.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 serde_json.workspace = true
 thiserror.workspace = true
 thiserror.workspace = true
 url.workspace = true
 url.workspace = true
 
 
-# Optional dependencies
-reqwest = { version = "0.12", default-features = false, features = ["json"], optional = true }
-bitreq = { version = "0.3.1", features = ["async", "async-https-rustls", "json-using-serde", "proxy"], optional = true }
-serde_urlencoded = { version = "0.7", optional = true }
-regex = { workspace = true, optional = true }
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+bitreq = { version = "0.3.1", features = ["async", "async-https-rustls", "json-using-serde", "proxy"] }
+serde_urlencoded = "0.7"
+regex.workspace = true
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-futures = "0.4"
+js-sys = "0.3"
+web-sys = { version = "0.3", features = ["Request", "RequestInit", "Response", "Headers"] }
+serde_urlencoded = "0.7"
 
 
 [dev-dependencies]
 [dev-dependencies]
 tokio = { workspace = true, features = ["rt", "macros"] }
 tokio = { workspace = true, features = ["rt", "macros"] }
@@ -35,6 +35,3 @@ serde = { workspace = true }
 
 
 [lints]
 [lints]
 workspace = true
 workspace = true
-
-[package.metadata.docs.rs]
-features = ["bitreq"]

+ 269 - 2
crates/cdk-http-client/src/backends/bitreq_backend.rs

@@ -1,4 +1,4 @@
-//! bitreq-based RequestBuilder implementation
+//! bitreq-based backend implementation
 
 
 use std::sync::Arc;
 use std::sync::Arc;
 
 
@@ -6,11 +6,232 @@ use bitreq::RequestExt;
 use serde::de::DeserializeOwned;
 use serde::de::DeserializeOwned;
 use serde::Serialize;
 use serde::Serialize;
 
 
-use crate::client::{apply_proxy_if_needed, ProxyConfig};
 use crate::error::HttpError;
 use crate::error::HttpError;
 use crate::request_builder_ext::RequestBuilderExt;
 use crate::request_builder_ext::RequestBuilderExt;
 use crate::response::{RawResponse, Response};
 use crate::response::{RawResponse, Response};
 
 
+#[derive(Debug, Clone)]
+pub(crate) struct ProxyConfig {
+    url: url::Url,
+    matcher: Option<regex::Regex>,
+}
+
+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 {
+            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 {
+            let proxy = bitreq::Proxy::new_http(&config.url)
+                .map_err(|e| HttpError::Proxy(e.to_string()))?;
+            return Ok(request.with_proxy(proxy));
+        }
+    }
+    Ok(request)
+}
+
+/// HTTP client wrapper
+#[derive(Clone)]
+pub struct HttpClient {
+    inner: Arc<bitreq::Client>,
+    proxy_config: Option<ProxyConfig>,
+}
+
+impl std::fmt::Debug for HttpClient {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("HttpClient").finish()
+    }
+}
+
+impl HttpClient {
+    /// Create a new HTTP client with default settings
+    pub fn new() -> Self {
+        Self {
+            inner: Arc::new(bitreq::Client::new(10)),
+            proxy_config: None,
+        }
+    }
+
+    /// Create an HTTP client from pre-built parts
+    pub(crate) fn from_parts(
+        client: Arc<bitreq::Client>,
+        proxy_config: Option<ProxyConfig>,
+    ) -> Self {
+        Self {
+            inner: client,
+            proxy_config,
+        }
+    }
+
+    /// Create a new HTTP client builder
+    pub fn builder() -> HttpClientBuilder {
+        HttpClientBuilder::default()
+    }
+
+    /// Helper method to apply proxy if URL matches the configured proxy rules
+    fn apply_proxy_if_needed(
+        &self,
+        request: bitreq::Request,
+        url: &str,
+    ) -> Response<bitreq::Request> {
+        apply_proxy_if_needed(request, url, &self.proxy_config)
+    }
+
+    /// GET request, returns JSON deserialized to R
+    pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
+        let request = bitreq::get(url);
+        let request = self.apply_proxy_if_needed(request, url)?;
+        let response = request
+            .send_async_with_client(&self.inner)
+            .await
+            .map_err(HttpError::from)?;
+        let status = response.status_code;
+
+        if !(200..300).contains(&status) {
+            let message = response.as_str().unwrap_or("").to_string();
+            return Err(HttpError::Status {
+                status: status as u16,
+                message,
+            });
+        }
+
+        response.json().map_err(HttpError::from)
+    }
+
+    /// POST with JSON body, returns JSON deserialized to R
+    pub async fn post_json<B: Serialize, R: DeserializeOwned>(
+        &self,
+        url: &str,
+        body: &B,
+    ) -> Response<R> {
+        let request = bitreq::post(url).with_json(body).map_err(HttpError::from)?;
+        let request = self.apply_proxy_if_needed(request, url)?;
+        let response: bitreq::Response = request
+            .send_async_with_client(&self.inner)
+            .await
+            .map_err(HttpError::from)?;
+        let status = response.status_code;
+
+        if !(200..300).contains(&status) {
+            let message = response.as_str().unwrap_or("").to_string();
+            return Err(HttpError::Status {
+                status: status as u16,
+                message,
+            });
+        }
+
+        response.json().map_err(HttpError::from)
+    }
+
+    /// POST with form data, returns JSON deserialized to R
+    pub async fn post_form<F: Serialize, R: DeserializeOwned>(
+        &self,
+        url: &str,
+        form: &F,
+    ) -> 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())
+            .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)
+            .await
+            .map_err(HttpError::from)?;
+        let status = response.status_code;
+
+        if !(200..300).contains(&status) {
+            let message = response.as_str().unwrap_or("").to_string();
+            return Err(HttpError::Status {
+                status: status as u16,
+                message,
+            });
+        }
+
+        response.json().map_err(HttpError::from)
+    }
+
+    /// PATCH with JSON body, returns JSON deserialized to R
+    pub async fn patch_json<B: Serialize, R: DeserializeOwned>(
+        &self,
+        url: &str,
+        body: &B,
+    ) -> Response<R> {
+        let request = bitreq::patch(url)
+            .with_json(body)
+            .map_err(HttpError::from)?;
+        let request = self.apply_proxy_if_needed(request, url)?;
+        let response: bitreq::Response = request
+            .send_async_with_client(&self.inner)
+            .await
+            .map_err(HttpError::from)?;
+        let status = response.status_code;
+
+        if !(200..300).contains(&status) {
+            let message = response.as_str().unwrap_or("").to_string();
+            return Err(HttpError::Status {
+                status: status as u16,
+                message,
+            });
+        }
+
+        response.json().map_err(HttpError::from)
+    }
+
+    /// GET request returning raw response body
+    pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
+        let request = bitreq::get(url);
+        let request = self.apply_proxy_if_needed(request, url)?;
+        let response = request
+            .send_async_with_client(&self.inner)
+            .await
+            .map_err(HttpError::from)?;
+        Ok(RawResponse::new(response))
+    }
+
+    /// POST request builder for complex cases
+    pub fn post(&self, url: &str) -> BitreqRequestBuilder {
+        BitreqRequestBuilder::new(
+            bitreq::post(url),
+            url,
+            self.inner.clone(),
+            self.proxy_config.clone(),
+        )
+    }
+
+    /// GET request builder for complex cases
+    pub fn get(&self, url: &str) -> BitreqRequestBuilder {
+        BitreqRequestBuilder::new(
+            bitreq::get(url),
+            url,
+            self.inner.clone(),
+            self.proxy_config.clone(),
+        )
+    }
+
+    /// PATCH request builder for complex cases
+    pub fn patch(&self, url: &str) -> BitreqRequestBuilder {
+        BitreqRequestBuilder::new(
+            bitreq::patch(url),
+            url,
+            self.inner.clone(),
+            self.proxy_config.clone(),
+        )
+    }
+}
+
 /// bitreq-based RequestBuilder wrapper
 /// bitreq-based RequestBuilder wrapper
 pub struct BitreqRequestBuilder {
 pub struct BitreqRequestBuilder {
     inner: bitreq::Request,
     inner: bitreq::Request,
@@ -116,3 +337,49 @@ impl RequestBuilderExt for BitreqRequestBuilder {
         response.json().map_err(HttpError::from)
         response.json().map_err(HttpError::from)
     }
     }
 }
 }
+
+/// HTTP client builder for configuring proxy and TLS settings
+#[derive(Debug, Default)]
+pub struct HttpClientBuilder {
+    proxy: Option<ProxyConfig>,
+    accept_invalid_certs: bool,
+}
+
+impl HttpClientBuilder {
+    /// Accept invalid TLS certificates
+    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
+        self.accept_invalid_certs = accept;
+        self
+    }
+
+    /// Set a proxy URL
+    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
+    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)
+    }
+
+    /// Build the HTTP client
+    pub fn build(self) -> Response<HttpClient> {
+        if self.accept_invalid_certs {
+            return Err(HttpError::Build(
+                "danger_accept_invalid_certs is not supported".to_string(),
+            ));
+        }
+
+        Ok(HttpClient::from_parts(
+            Arc::new(bitreq::Client::new(10)),
+            self.proxy,
+        ))
+    }
+}

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

@@ -1,12 +1,12 @@
 //! HTTP request builder backends
 //! HTTP request builder backends
 
 
-#[cfg(feature = "bitreq")]
+#[cfg(not(target_arch = "wasm32"))]
 pub mod bitreq_backend;
 pub mod bitreq_backend;
 
 
-#[cfg(feature = "reqwest")]
-pub mod reqwest_backend;
+#[cfg(target_arch = "wasm32")]
+pub mod wasm_backend;
 
 
-#[cfg(feature = "bitreq")]
-pub use bitreq_backend::BitreqRequestBuilder;
-#[cfg(feature = "reqwest")]
-pub use reqwest_backend::ReqwestRequestBuilder;
+#[cfg(not(target_arch = "wasm32"))]
+pub use bitreq_backend::{BitreqRequestBuilder, HttpClient, HttpClientBuilder};
+#[cfg(target_arch = "wasm32")]
+pub use wasm_backend::{HttpClient, HttpClientBuilder, WasmRequestBuilder};

+ 0 - 61
crates/cdk-http-client/src/backends/reqwest_backend.rs

@@ -1,61 +0,0 @@
-//! reqwest-based RequestBuilder implementation
-
-use serde::de::DeserializeOwned;
-use serde::Serialize;
-
-use crate::error::HttpError;
-use crate::request_builder_ext::RequestBuilderExt;
-use crate::response::{RawResponse, Response};
-
-/// reqwest-based RequestBuilder wrapper
-#[derive(Debug)]
-pub struct ReqwestRequestBuilder {
-    inner: reqwest::RequestBuilder,
-}
-
-impl ReqwestRequestBuilder {
-    /// Create a new ReqwestRequestBuilder from a reqwest::RequestBuilder
-    pub(crate) fn new(inner: reqwest::RequestBuilder) -> Self {
-        Self { inner }
-    }
-}
-
-impl RequestBuilderExt for ReqwestRequestBuilder {
-    fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
-        Self {
-            inner: self.inner.header(key.as_ref(), value.as_ref()),
-        }
-    }
-
-    fn json<T: Serialize>(self, body: &T) -> Self {
-        Self {
-            inner: self.inner.json(body),
-        }
-    }
-
-    fn form<T: Serialize>(self, body: &T) -> Self {
-        Self {
-            inner: self.inner.form(body),
-        }
-    }
-
-    async fn send(self) -> Response<RawResponse> {
-        let response = self.inner.send().await.map_err(HttpError::from)?;
-        Ok(RawResponse::new(response))
-    }
-
-    async fn send_json<R: DeserializeOwned>(self) -> Response<R> {
-        let response = self.inner.send().await.map_err(HttpError::from)?;
-        let status = response.status();
-
-        if !status.is_success() {
-            let message = response.text().await.unwrap_or_default();
-            return Err(HttpError::Status {
-                status: status.as_u16(),
-                message,
-            });
-        }
-
-        response.json().await.map_err(HttpError::from)
-    }
-}

+ 247 - 0
crates/cdk-http-client/src/backends/wasm_backend.rs

@@ -0,0 +1,247 @@
+//! WASM fetch-based backend implementation
+
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsCast;
+use wasm_bindgen_futures::JsFuture;
+
+use crate::error::HttpError;
+use crate::request_builder_ext::RequestBuilderExt;
+use crate::response::{RawResponse, Response};
+
+#[wasm_bindgen]
+extern "C" {
+    #[wasm_bindgen(js_name = "fetch")]
+    fn js_fetch(input: &web_sys::Request) -> js_sys::Promise;
+}
+
+/// HTTP client wrapper
+#[derive(Clone, Debug)]
+pub struct HttpClient;
+
+impl HttpClient {
+    /// Create a new HTTP client with default settings
+    pub fn new() -> Self {
+        Self
+    }
+
+    /// Create a new HTTP client builder
+    pub fn builder() -> HttpClientBuilder {
+        HttpClientBuilder::default()
+    }
+
+    /// GET request, returns JSON deserialized to R
+    pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
+        WasmRequestBuilder::new("GET", url).send_json().await
+    }
+
+    /// POST with JSON body, returns JSON deserialized to R
+    pub async fn post_json<B: Serialize, R: DeserializeOwned>(
+        &self,
+        url: &str,
+        body: &B,
+    ) -> Response<R> {
+        WasmRequestBuilder::new("POST", url)
+            .json(body)
+            .send_json()
+            .await
+    }
+
+    /// POST with form data, returns JSON deserialized to R
+    pub async fn post_form<F: Serialize, R: DeserializeOwned>(
+        &self,
+        url: &str,
+        form: &F,
+    ) -> Response<R> {
+        WasmRequestBuilder::new("POST", url)
+            .form(form)
+            .send_json()
+            .await
+    }
+
+    /// PATCH with JSON body, returns JSON deserialized to R
+    pub async fn patch_json<B: Serialize, R: DeserializeOwned>(
+        &self,
+        url: &str,
+        body: &B,
+    ) -> Response<R> {
+        WasmRequestBuilder::new("PATCH", url)
+            .json(body)
+            .send_json()
+            .await
+    }
+
+    /// GET request returning raw response body
+    pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
+        WasmRequestBuilder::new("GET", url).send().await
+    }
+
+    /// POST request builder for complex cases
+    pub fn post(&self, url: &str) -> WasmRequestBuilder {
+        WasmRequestBuilder::new("POST", url)
+    }
+
+    /// GET request builder for complex cases
+    pub fn get(&self, url: &str) -> WasmRequestBuilder {
+        WasmRequestBuilder::new("GET", url)
+    }
+
+    /// PATCH request builder for complex cases
+    pub fn patch(&self, url: &str) -> WasmRequestBuilder {
+        WasmRequestBuilder::new("PATCH", url)
+    }
+}
+
+/// WASM fetch-based RequestBuilder wrapper
+#[derive(Debug)]
+pub struct WasmRequestBuilder {
+    method: String,
+    url: String,
+    headers: Vec<(String, String)>,
+    body: Option<WasmBody>,
+}
+
+#[derive(Debug)]
+enum WasmBody {
+    Json(Vec<u8>),
+    Form(String),
+}
+
+impl WasmRequestBuilder {
+    /// Create a new WasmRequestBuilder
+    pub(crate) fn new(method: &str, url: &str) -> Self {
+        Self {
+            method: method.to_string(),
+            url: url.to_string(),
+            headers: Vec::new(),
+            body: None,
+        }
+    }
+
+    async fn execute(self) -> Response<RawResponse> {
+        let opts = web_sys::RequestInit::new();
+        opts.set_method(&self.method);
+
+        if let Some(body) = &self.body {
+            match body {
+                WasmBody::Json(bytes) => {
+                    let uint8_array = js_sys::Uint8Array::from(bytes.as_slice());
+                    opts.set_body(&uint8_array.into());
+                }
+                WasmBody::Form(form_str) => {
+                    opts.set_body(&JsValue::from_str(form_str));
+                }
+            }
+        }
+
+        let request = web_sys::Request::new_with_str_and_init(&self.url, &opts)
+            .map_err(|e| HttpError::Other(format!("Failed to create request: {:?}", e)))?;
+
+        let headers = request.headers();
+        for (key, value) in &self.headers {
+            headers
+                .set(key, value)
+                .map_err(|e| HttpError::Other(format!("Failed to set header: {:?}", e)))?;
+        }
+
+        let resp_value = JsFuture::from(js_fetch(&request))
+            .await
+            .map_err(|e| HttpError::Connection(format!("Fetch failed: {:?}", e)))?;
+
+        let resp: web_sys::Response = resp_value
+            .dyn_into()
+            .map_err(|_| HttpError::Other("Response is not a web_sys::Response".to_string()))?;
+
+        let status = resp.status();
+
+        let body_promise = resp
+            .array_buffer()
+            .map_err(|e| HttpError::Other(format!("Failed to read body: {:?}", e)))?;
+
+        let body_value = JsFuture::from(body_promise)
+            .await
+            .map_err(|e| HttpError::Other(format!("Failed to read body: {:?}", e)))?;
+
+        let body_array = js_sys::Uint8Array::new(&body_value);
+        let body = body_array.to_vec();
+
+        Ok(RawResponse::new(status, body))
+    }
+}
+
+impl RequestBuilderExt for WasmRequestBuilder {
+    fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
+        self.headers
+            .push((key.as_ref().to_string(), value.as_ref().to_string()));
+        self
+    }
+
+    fn json<T: Serialize>(mut self, body: &T) -> Self {
+        match serde_json::to_vec(body) {
+            Ok(bytes) => {
+                self.body = Some(WasmBody::Json(bytes));
+                self.headers
+                    .push(("Content-Type".to_string(), "application/json".to_string()));
+            }
+            Err(_) => {} // Error will surface when trying to send
+        }
+        self
+    }
+
+    fn form<T: Serialize>(mut self, body: &T) -> Self {
+        match serde_urlencoded::to_string(body) {
+            Ok(form_str) => {
+                self.body = Some(WasmBody::Form(form_str));
+                self.headers.push((
+                    "Content-Type".to_string(),
+                    "application/x-www-form-urlencoded".to_string(),
+                ));
+            }
+            Err(_) => {}
+        }
+        self
+    }
+
+    async fn send(self) -> Response<RawResponse> {
+        self.execute().await
+    }
+
+    async fn send_json<R: DeserializeOwned>(self) -> Response<R> {
+        let raw = self.execute().await?;
+        let status = raw.status();
+
+        if !raw.is_success() {
+            let message = String::from_utf8_lossy(&raw.body).to_string();
+            return Err(HttpError::Status { status, message });
+        }
+
+        serde_json::from_slice(&raw.body).map_err(HttpError::from)
+    }
+}
+
+/// HTTP client builder for configuring proxy and TLS settings
+#[derive(Debug, Default)]
+pub struct HttpClientBuilder;
+
+impl HttpClientBuilder {
+    /// Accept invalid TLS certificates (not supported on WASM)
+    pub fn danger_accept_invalid_certs(self, _accept: bool) -> Self {
+        self
+    }
+
+    /// Set a proxy URL (not supported on WASM)
+    pub fn proxy(self, _url: url::Url) -> Self {
+        panic!("proxy configuration is not supported on WASM")
+    }
+
+    /// Set a proxy URL with a host pattern matcher (not supported on WASM)
+    pub fn proxy_with_matcher(self, _url: url::Url, _pattern: &str) -> Response<Self> {
+        panic!("proxy configuration is not supported on WASM")
+    }
+
+    /// Build the HTTP client
+    pub fn build(self) -> Response<HttpClient> {
+        Ok(HttpClient)
+    }
+}

+ 4 - 490
crates/cdk-http-client/src/client.rs

@@ -1,337 +1,9 @@
 //! HTTP client wrapper
 //! HTTP client wrapper
 
 
-#[cfg(feature = "bitreq")]
-use std::sync::Arc;
-
-#[cfg(feature = "bitreq")]
-use bitreq::RequestExt;
 use serde::de::DeserializeOwned;
 use serde::de::DeserializeOwned;
-use serde::Serialize;
-
-use crate::error::HttpError;
-use crate::request::RequestBuilder;
-use crate::response::{RawResponse, Response};
-
-/// HTTP client wrapper
-#[derive(Clone)]
-pub struct HttpClient {
-    #[cfg(feature = "reqwest")]
-    inner: reqwest::Client,
-    #[cfg(feature = "bitreq")]
-    inner: Arc<bitreq::Client>,
-    #[cfg(feature = "bitreq")]
-    proxy_config: Option<ProxyConfig>,
-}
-
-impl std::fmt::Debug for HttpClient {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("HttpClient").finish()
-    }
-}
-
-#[cfg(feature = "reqwest")]
-impl HttpClient {
-    /// Create a new HTTP client with default settings
-    pub fn new() -> Self {
-        Self {
-            inner: reqwest::Client::new(),
-        }
-    }
-
-    /// Create an HttpClient from a reqwest::Client
-    pub fn from_reqwest(client: reqwest::Client) -> Self {
-        Self { inner: client }
-    }
-
-    /// Create a new HTTP client builder
-    pub fn builder() -> HttpClientBuilder {
-        HttpClientBuilder::default()
-    }
-
-    /// GET request, returns JSON deserialized to R
-    pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
-        let response = self.inner.get(url).send().await.map_err(HttpError::from)?;
-        let status = response.status();
-
-        if !status.is_success() {
-            let message = response.text().await.unwrap_or_default();
-            return Err(HttpError::Status {
-                status: status.as_u16(),
-                message,
-            });
-        }
-
-        response.json().await.map_err(HttpError::from)
-    }
-
-    /// POST with JSON body, returns JSON deserialized to R
-    pub async fn post_json<B: Serialize + ?Sized, R: DeserializeOwned>(
-        &self,
-        url: &str,
-        body: &B,
-    ) -> Response<R> {
-        let response = self
-            .inner
-            .post(url)
-            .json(body)
-            .send()
-            .await
-            .map_err(HttpError::from)?;
-        let status = response.status();
-
-        if !status.is_success() {
-            let message = response.text().await.unwrap_or_default();
-            return Err(HttpError::Status {
-                status: status.as_u16(),
-                message,
-            });
-        }
-
-        response.json().await.map_err(HttpError::from)
-    }
-
-    /// POST with form data, returns JSON deserialized to R
-    pub async fn post_form<F: Serialize + ?Sized, R: DeserializeOwned>(
-        &self,
-        url: &str,
-        form: &F,
-    ) -> Response<R> {
-        let response = self
-            .inner
-            .post(url)
-            .form(form)
-            .send()
-            .await
-            .map_err(HttpError::from)?;
-        let status = response.status();
-
-        if !status.is_success() {
-            let message = response.text().await.unwrap_or_default();
-            return Err(HttpError::Status {
-                status: status.as_u16(),
-                message,
-            });
-        }
-
-        response.json().await.map_err(HttpError::from)
-    }
-
-    /// PATCH with JSON body, returns JSON deserialized to R
-    pub async fn patch_json<B: Serialize + ?Sized, R: DeserializeOwned>(
-        &self,
-        url: &str,
-        body: &B,
-    ) -> Response<R> {
-        let response = self
-            .inner
-            .patch(url)
-            .json(body)
-            .send()
-            .await
-            .map_err(HttpError::from)?;
-        let status = response.status();
-
-        if !status.is_success() {
-            let message = response.text().await.unwrap_or_default();
-            return Err(HttpError::Status {
-                status: status.as_u16(),
-                message,
-            });
-        }
-
-        response.json().await.map_err(HttpError::from)
-    }
-
-    /// GET request returning raw response body
-    pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
-        let response = self.inner.get(url).send().await.map_err(HttpError::from)?;
-        Ok(RawResponse::new(response))
-    }
-
-    /// POST request builder for complex cases
-    pub fn post(&self, url: &str) -> RequestBuilder {
-        RequestBuilder::new(self.inner.post(url))
-    }
-
-    /// GET request builder for complex cases
-    pub fn get(&self, url: &str) -> RequestBuilder {
-        RequestBuilder::new(self.inner.get(url))
-    }
-
-    /// PATCH request builder for complex cases
-    pub fn patch(&self, url: &str) -> RequestBuilder {
-        RequestBuilder::new(self.inner.patch(url))
-    }
-}
-
-#[cfg(feature = "bitreq")]
-impl HttpClient {
-    /// Create a new HTTP client with default settings
-    pub fn new() -> Self {
-        Self {
-            inner: Arc::new(bitreq::Client::new(10)), // Default capacity of 10
-            proxy_config: None,
-        }
-    }
-
-    /// Create a new HTTP client builder
-    pub fn builder() -> HttpClientBuilder {
-        HttpClientBuilder::default()
-    }
-
-    /// Helper method to apply proxy if URL matches the configured proxy rules
-    fn apply_proxy_if_needed(
-        &self,
-        request: bitreq::Request,
-        url: &str,
-    ) -> Response<bitreq::Request> {
-        apply_proxy_if_needed(request, url, &self.proxy_config)
-    }
-
-    /// GET request, returns JSON deserialized to R
-    pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
-        let request = bitreq::get(url);
-        let request = self.apply_proxy_if_needed(request, url)?;
-        let response = request
-            .send_async_with_client(&self.inner)
-            .await
-            .map_err(HttpError::from)?;
-        let status = response.status_code;
 
 
-        if !(200..300).contains(&status) {
-            let message = response.as_str().unwrap_or("").to_string();
-            return Err(HttpError::Status {
-                status: status as u16,
-                message,
-            });
-        }
-
-        response.json().map_err(HttpError::from)
-    }
-
-    /// POST with JSON body, returns JSON deserialized to R
-    pub async fn post_json<B: Serialize, R: DeserializeOwned>(
-        &self,
-        url: &str,
-        body: &B,
-    ) -> Response<R> {
-        let request = bitreq::post(url).with_json(body).map_err(HttpError::from)?;
-        let request = self.apply_proxy_if_needed(request, url)?;
-        let response: bitreq::Response = request
-            .send_async_with_client(&self.inner)
-            .await
-            .map_err(HttpError::from)?;
-        let status = response.status_code;
-
-        if !(200..300).contains(&status) {
-            let message = response.as_str().unwrap_or("").to_string();
-            return Err(HttpError::Status {
-                status: status as u16,
-                message,
-            });
-        }
-
-        response.json().map_err(HttpError::from)
-    }
-
-    /// POST with form data, returns JSON deserialized to R
-    pub async fn post_form<F: Serialize, R: DeserializeOwned>(
-        &self,
-        url: &str,
-        form: &F,
-    ) -> 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())
-            .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)
-            .await
-            .map_err(HttpError::from)?;
-        let status = response.status_code;
-
-        if !(200..300).contains(&status) {
-            let message = response.as_str().unwrap_or("").to_string();
-            return Err(HttpError::Status {
-                status: status as u16,
-                message,
-            });
-        }
-
-        response.json().map_err(HttpError::from)
-    }
-
-    /// PATCH with JSON body, returns JSON deserialized to R
-    pub async fn patch_json<B: Serialize, R: DeserializeOwned>(
-        &self,
-        url: &str,
-        body: &B,
-    ) -> Response<R> {
-        let request = bitreq::patch(url)
-            .with_json(body)
-            .map_err(HttpError::from)?;
-        let request = self.apply_proxy_if_needed(request, url)?;
-        let response: bitreq::Response = request
-            .send_async_with_client(&self.inner)
-            .await
-            .map_err(HttpError::from)?;
-        let status = response.status_code;
-
-        if !(200..300).contains(&status) {
-            let message = response.as_str().unwrap_or("").to_string();
-            return Err(HttpError::Status {
-                status: status as u16,
-                message,
-            });
-        }
-
-        response.json().map_err(HttpError::from)
-    }
-
-    /// GET request returning raw response body
-    pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
-        let request = bitreq::get(url);
-        let request = self.apply_proxy_if_needed(request, url)?;
-        let response = request
-            .send_async_with_client(&self.inner)
-            .await
-            .map_err(HttpError::from)?;
-        Ok(RawResponse::new(response))
-    }
-
-    /// 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),
-            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),
-            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),
-            url,
-            self.inner.clone(),
-            self.proxy_config.clone(),
-        )
-    }
-}
+pub use crate::backends::{HttpClient, HttpClientBuilder};
+use crate::response::Response;
 
 
 impl Default for HttpClient {
 impl Default for HttpClient {
     fn default() -> Self {
     fn default() -> Self {
@@ -339,154 +11,6 @@ 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_attr(target_arch = "wasm32", allow(dead_code))]
-#[cfg(any(feature = "bitreq", feature = "reqwest"))]
-#[derive(Debug, Clone)]
-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(all(feature = "reqwest", not(target_arch = "wasm32")))]
-    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(all(feature = "reqwest", not(target_arch = "wasm32")))]
-    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 {
-        self.accept_invalid_certs = accept;
-        self
-    }
-
-    /// Set a proxy URL (bitreq only)
-    #[cfg(feature = "bitreq")]
-    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 (bitreq only)
-    #[cfg(feature = "bitreq")]
-    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)
-    }
-
-    /// Build the HTTP client
-    pub fn build(self) -> Response<HttpClient> {
-        #[cfg(feature = "reqwest")]
-        {
-            #[cfg(not(target_arch = "wasm32"))]
-            let mut builder = reqwest::Client::builder();
-            #[cfg(target_arch = "wasm32")]
-            let builder = reqwest::Client::builder();
-            #[cfg(not(target_arch = "wasm32"))]
-            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);
-                }
-            }
-            #[cfg(target_arch = "wasm32")]
-            if self.proxy.is_some() {
-                return Err(HttpError::Build(
-                    "proxy configuration is not supported on wasm".to_string(),
-                ));
-            }
-            let client = builder
-                .build()
-                .map_err(|e| HttpError::Build(e.to_string()))?;
-            Ok(HttpClient { inner: client })
-        }
-
-        #[cfg(feature = "bitreq")]
-        {
-            // Return error if danger_accept_invalid_certs was set
-            if self.accept_invalid_certs {
-                return Err(HttpError::Build(
-                    "danger_accept_invalid_certs is not supported".to_string(),
-                ));
-            }
-
-            Ok(HttpClient {
-                inner: Arc::new(bitreq::Client::new(10)), // Default capacity of 10
-                proxy_config: self.proxy,
-            })
-        }
-    }
-}
-
 /// Convenience function for simple GET requests
 /// Convenience function for simple GET requests
 pub async fn fetch<R: DeserializeOwned>(url: &str) -> Response<R> {
 pub async fn fetch<R: DeserializeOwned>(url: &str) -> Response<R> {
     HttpClient::new().fetch(url).await
     HttpClient::new().fetch(url).await
@@ -499,14 +23,12 @@ mod tests {
     #[test]
     #[test]
     fn test_client_new() {
     fn test_client_new() {
         let client = HttpClient::new();
         let client = HttpClient::new();
-        // Client should be constructable without panicking
         let _ = format!("{:?}", client);
         let _ = format!("{:?}", client);
     }
     }
 
 
     #[test]
     #[test]
     fn test_client_default() {
     fn test_client_default() {
         let client = HttpClient::default();
         let client = HttpClient::default();
-        // Default should produce a valid client
         let _ = format!("{:?}", client);
         let _ = format!("{:?}", client);
     }
     }
 
 
@@ -522,17 +44,10 @@ mod tests {
         assert!(result.is_ok());
         assert!(result.is_ok());
     }
     }
 
 
-    #[cfg(feature = "reqwest")]
-    #[test]
-    fn test_from_reqwest() {
-        let reqwest_client = reqwest::Client::new();
-        let client = HttpClient::from_reqwest(reqwest_client);
-        let _ = format!("{:?}", client);
-    }
-
-    #[cfg(feature = "bitreq")]
+    #[cfg(not(target_arch = "wasm32"))]
     mod bitreq_tests {
     mod bitreq_tests {
         use super::*;
         use super::*;
+        use crate::error::HttpError;
 
 
         #[test]
         #[test]
         fn test_builder_accept_invalid_certs_returns_error() {
         fn test_builder_accept_invalid_certs_returns_error() {
@@ -577,7 +92,6 @@ mod tests {
         #[test]
         #[test]
         fn test_builder_proxy_with_invalid_matcher() {
         fn test_builder_proxy_with_invalid_matcher() {
             let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
             let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
-            // Invalid regex pattern (unclosed bracket)
             let result = HttpClientBuilder::default().proxy_with_matcher(proxy_url, r"[invalid");
             let result = HttpClientBuilder::default().proxy_with_matcher(proxy_url, r"[invalid");
             assert!(result.is_err());
             assert!(result.is_err());
 
 

+ 1 - 19
crates/cdk-http-client/src/error.rs

@@ -33,25 +33,7 @@ pub enum HttpError {
     Other(String),
     Other(String),
 }
 }
 
 
-#[cfg(feature = "reqwest")]
-impl From<reqwest::Error> for HttpError {
-    fn from(err: reqwest::Error) -> Self {
-        if err.is_timeout() {
-            HttpError::Timeout
-        } else if err.is_builder() {
-            HttpError::Build(err.to_string())
-        } else if let Some(status) = err.status() {
-            HttpError::Status {
-                status: status.as_u16(),
-                message: err.to_string(),
-            }
-        } else {
-            HttpError::Other(err.to_string())
-        }
-    }
-}
-
-#[cfg(feature = "bitreq")]
+#[cfg(not(target_arch = "wasm32"))]
 impl From<bitreq::Error> for HttpError {
 impl From<bitreq::Error> for HttpError {
     fn from(err: bitreq::Error) -> Self {
     fn from(err: bitreq::Error) -> Self {
         use std::io;
         use std::io;

+ 4 - 7
crates/cdk-http-client/src/lib.rs

@@ -1,7 +1,7 @@
 //! HTTP client abstraction for CDK
 //! HTTP client abstraction for CDK
 //!
 //!
 //! This crate provides an HTTP client wrapper that abstracts the underlying HTTP library
 //! This crate provides an HTTP client wrapper that abstracts the underlying HTTP library
-//! (reqwest or bitreq).
+//! (bitreq on native, fetch API on WASM).
 //! Using this crate allows other CDK crates to avoid direct dependencies on a specific backend.
 //! Using this crate allows other CDK crates to avoid direct dependencies on a specific backend.
 //!
 //!
 //! # Example
 //! # Example
@@ -21,9 +21,6 @@
 //! }
 //! }
 //! ```
 //! ```
 
 
-#[cfg(all(feature = "reqwest", feature = "bitreq"))]
-compile_error!("Features \"reqwest\" and \"bitreq\" are mutually exclusive. Enable only one.");
-
 mod backends;
 mod backends;
 mod client;
 mod client;
 mod error;
 mod error;
@@ -31,10 +28,10 @@ mod request;
 mod request_builder_ext;
 mod request_builder_ext;
 mod response;
 mod response;
 
 
-#[cfg(feature = "bitreq")]
+#[cfg(not(target_arch = "wasm32"))]
 pub use backends::BitreqRequestBuilder;
 pub use backends::BitreqRequestBuilder;
-#[cfg(feature = "reqwest")]
-pub use backends::ReqwestRequestBuilder;
+#[cfg(target_arch = "wasm32")]
+pub use backends::WasmRequestBuilder;
 pub use client::{fetch, HttpClient, HttpClientBuilder};
 pub use client::{fetch, HttpClient, HttpClientBuilder};
 pub use error::HttpError;
 pub use error::HttpError;
 pub use request::{RequestBuilder, RequestBuilderExt};
 pub use request::{RequestBuilder, RequestBuilderExt};

+ 4 - 4
crates/cdk-http-client/src/request.rs

@@ -3,9 +3,9 @@
 /// HTTP request builder for complex requests
 /// HTTP request builder for complex requests
 ///
 ///
 /// This is a type alias that resolves to either `BitreqRequestBuilder` or
 /// This is a type alias that resolves to either `BitreqRequestBuilder` or
-/// `ReqwestRequestBuilder` depending on the enabled feature.
-#[cfg(feature = "bitreq")]
+/// `WasmRequestBuilder` depending on the target architecture.
+#[cfg(not(target_arch = "wasm32"))]
 pub use crate::backends::BitreqRequestBuilder as RequestBuilder;
 pub use crate::backends::BitreqRequestBuilder as RequestBuilder;
-#[cfg(feature = "reqwest")]
-pub use crate::backends::ReqwestRequestBuilder as RequestBuilder;
+#[cfg(target_arch = "wasm32")]
+pub use crate::backends::WasmRequestBuilder as RequestBuilder;
 pub use crate::request_builder_ext::RequestBuilderExt;
 pub use crate::request_builder_ext::RequestBuilderExt;

+ 11 - 18
crates/cdk-http-client/src/response.rs

@@ -12,20 +12,17 @@ pub type Response<R, E = HttpError> = Result<R, E>;
 #[derive(Debug)]
 #[derive(Debug)]
 pub struct RawResponse {
 pub struct RawResponse {
     status: u16,
     status: u16,
-    #[cfg(feature = "reqwest")]
-    inner: reqwest::Response,
-    #[cfg(feature = "bitreq")]
+    #[cfg(not(target_arch = "wasm32"))]
     inner: bitreq::Response,
     inner: bitreq::Response,
+    #[cfg(target_arch = "wasm32")]
+    pub(crate) body: Vec<u8>,
 }
 }
 
 
-#[cfg(feature = "reqwest")]
+#[cfg(target_arch = "wasm32")]
 impl RawResponse {
 impl RawResponse {
-    /// Create a new RawResponse from a reqwest::Response
-    pub(crate) fn new(response: reqwest::Response) -> Self {
-        Self {
-            status: response.status().as_u16(),
-            inner: response,
-        }
+    /// Create a new RawResponse from status and body bytes
+    pub(crate) fn new(status: u16, body: Vec<u8>) -> Self {
+        Self { status, body }
     }
     }
 
 
     /// Get the HTTP status code
     /// Get the HTTP status code
@@ -50,25 +47,21 @@ impl RawResponse {
 
 
     /// Get the response body as text
     /// Get the response body as text
     pub async fn text(self) -> Response<String> {
     pub async fn text(self) -> Response<String> {
-        self.inner.text().await.map_err(HttpError::from)
+        String::from_utf8(self.body).map_err(|e| HttpError::Other(e.to_string()))
     }
     }
 
 
     /// Get the response body as JSON
     /// Get the response body as JSON
     pub async fn json<T: DeserializeOwned>(self) -> Response<T> {
     pub async fn json<T: DeserializeOwned>(self) -> Response<T> {
-        self.inner.json().await.map_err(HttpError::from)
+        serde_json::from_slice(&self.body).map_err(HttpError::from)
     }
     }
 
 
     /// Get the response body as bytes
     /// Get the response body as bytes
     pub async fn bytes(self) -> Response<Vec<u8>> {
     pub async fn bytes(self) -> Response<Vec<u8>> {
-        self.inner
-            .bytes()
-            .await
-            .map(|b| b.to_vec())
-            .map_err(HttpError::from)
+        Ok(self.body)
     }
     }
 }
 }
 
 
-#[cfg(feature = "bitreq")]
+#[cfg(not(target_arch = "wasm32"))]
 impl RawResponse {
 impl RawResponse {
     /// Create a new RawResponse from a bitreq::Response
     /// Create a new RawResponse from a bitreq::Response
     pub(crate) fn new(response: bitreq::Response) -> Self {
     pub(crate) fn new(response: bitreq::Response) -> Self {

+ 0 - 1
crates/cdk-prometheus/Cargo.toml

@@ -40,7 +40,6 @@ once_cell.workspace = true
 
 
 [dev-dependencies]
 [dev-dependencies]
 tokio = { workspace = true, features = ["full"] }
 tokio = { workspace = true, features = ["full"] }
-reqwest.workspace = true
 tracing-subscriber.workspace = true
 tracing-subscriber.workspace = true
 
 
 [lints]
 [lints]

+ 2 - 2
crates/cdk/Cargo.toml

@@ -64,7 +64,7 @@ zeroize = "1"
 tokio-util.workspace = true
 tokio-util.workspace = true
 
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-cdk-http-client = { workspace = true, features = ["bitreq"] }
+cdk-http-client.workspace = true
 hickory-resolver = { version = "0.25.2", optional = true, features = ["dnssec-ring"] }
 hickory-resolver = { version = "0.25.2", optional = true, features = ["dnssec-ring"] }
 tokio = { workspace = true, features = [
 tokio = { workspace = true, features = [
     "rt-multi-thread",
     "rt-multi-thread",
@@ -90,7 +90,7 @@ tls-api = { version = "0.9", optional = true }
 tls-api-native-tls = { version = "0.9", optional = true }
 tls-api-native-tls = { version = "0.9", optional = true }
 
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-cdk-http-client = { workspace = true, default-features = false, features = ["reqwest"] }
+cdk-http-client = { workspace = true, default-features = false }
 tokio = { workspace = true, default-features = false, features = ["rt", "macros", "sync", "time"] }
 tokio = { workspace = true, default-features = false, features = ["rt", "macros", "sync", "time"] }
 cdk-signatory = { workspace = true, default-features = false }
 cdk-signatory = { workspace = true, default-features = false }
 getrandom = { version = "0.2", features = ["js"] }
 getrandom = { version = "0.2", features = ["js"] }