Browse Source

Wasm http client

Cesar Rodas 5 days ago
parent
commit
7863a0ed40

+ 4 - 0
Cargo.lock

@@ -1384,6 +1384,7 @@ dependencies = [
 name = "cdk-http-client"
 version = "0.15.0"
 dependencies = [
+ "js-sys",
  "mockito",
  "regex",
  "reqwest",
@@ -1392,6 +1393,9 @@ dependencies = [
  "thiserror 2.0.18",
  "tokio",
  "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
 ]
 
 [[package]]

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

@@ -21,7 +21,15 @@ reqwest = { workspace = true }
 regex = "1"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-reqwest = { version = "0.12", default-features = false, features = ["json"] }
+wasm-bindgen = "0.2"
+wasm-bindgen-futures = "0.4"
+js-sys = "0.3"
+web-sys = { version = "0.3", features = [
+    "Headers",
+    "Request",
+    "RequestInit",
+    "Response",
+] }
 
 [dev-dependencies]
 tokio = { workspace = true, features = ["rt", "macros"] }

+ 73 - 94
crates/cdk-http-client/src/client.rs

@@ -5,7 +5,8 @@ use serde::Serialize;
 
 use crate::error::HttpError;
 use crate::request::RequestBuilder;
-use crate::response::{RawResponse, Response};
+use crate::response::RawResponse;
+use crate::Response;
 
 /// HTTP client wrapper
 #[derive(Debug, Clone)]
@@ -147,13 +148,10 @@ impl HttpClient {
 /// HTTP client builder for configuring proxy and TLS settings
 #[derive(Debug, Default)]
 pub struct HttpClientBuilder {
-    #[cfg(not(target_arch = "wasm32"))]
     accept_invalid_certs: bool,
-    #[cfg(not(target_arch = "wasm32"))]
     proxy: Option<ProxyConfig>,
 }
 
-#[cfg(not(target_arch = "wasm32"))]
 #[derive(Debug)]
 struct ProxyConfig {
     url: url::Url,
@@ -161,22 +159,19 @@ struct ProxyConfig {
 }
 
 impl HttpClientBuilder {
-    /// Accept invalid TLS certificates (non-WASM only)
-    #[cfg(not(target_arch = "wasm32"))]
+    /// 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 (non-WASM only)
-    #[cfg(not(target_arch = "wasm32"))]
+    /// 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 (non-WASM only)
-    #[cfg(not(target_arch = "wasm32"))]
+    /// 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)))?;
@@ -189,35 +184,27 @@ impl HttpClientBuilder {
 
     /// Build the HTTP client
     pub fn build(self) -> Response<HttpClient> {
-        #[cfg(not(target_arch = "wasm32"))]
-        {
-            let mut builder =
-                reqwest::Client::builder().danger_accept_invalid_certs(self.accept_invalid_certs);
-
-            if let Some(proxy_config) = self.proxy {
-                let proxy_url = proxy_config.url.to_string();
-                let proxy = if let Some(matcher) = proxy_config.matcher {
-                    reqwest::Proxy::custom(move |url| {
-                        if matcher.is_match(url.host_str().unwrap_or("")) {
-                            Some(proxy_url.clone())
-                        } else {
-                            None
-                        }
-                    })
-                } else {
-                    reqwest::Proxy::all(&proxy_url).map_err(|e| HttpError::Proxy(e.to_string()))?
-                };
-                builder = builder.proxy(proxy);
-            }
-
-            let client = builder.build().map_err(HttpError::from)?;
-            Ok(HttpClient { inner: client })
+        let mut builder =
+            reqwest::Client::builder().danger_accept_invalid_certs(self.accept_invalid_certs);
+
+        if let Some(proxy_config) = self.proxy {
+            let proxy_url = proxy_config.url.to_string();
+            let proxy = if let Some(matcher) = proxy_config.matcher {
+                reqwest::Proxy::custom(move |url| {
+                    if matcher.is_match(url.host_str().unwrap_or("")) {
+                        Some(proxy_url.clone())
+                    } else {
+                        None
+                    }
+                })
+            } else {
+                reqwest::Proxy::all(&proxy_url).map_err(|e| HttpError::Proxy(e.to_string()))?
+            };
+            builder = builder.proxy(proxy);
         }
 
-        #[cfg(target_arch = "wasm32")]
-        {
-            Ok(HttpClient::new())
-        }
+        let client = builder.build().map_err(HttpError::from)?;
+        Ok(HttpClient { inner: client })
     }
 }
 
@@ -233,14 +220,12 @@ mod tests {
     #[test]
     fn test_client_new() {
         let client = HttpClient::new();
-        // Client should be constructable without panicking
         let _ = format!("{:?}", client);
     }
 
     #[test]
     fn test_client_default() {
         let client = HttpClient::default();
-        // Default should produce a valid client
         let _ = format!("{:?}", client);
     }
 
@@ -263,67 +248,61 @@ mod tests {
         let _ = format!("{:?}", client);
     }
 
-    #[cfg(not(target_arch = "wasm32"))]
-    mod non_wasm {
-        use super::*;
-
-        #[test]
-        fn test_builder_accept_invalid_certs() {
-            let result = HttpClientBuilder::default()
-                .danger_accept_invalid_certs(true)
-                .build();
-            assert!(result.is_ok());
-        }
-
-        #[test]
-        fn test_builder_accept_invalid_certs_false() {
-            let result = HttpClientBuilder::default()
-                .danger_accept_invalid_certs(false)
-                .build();
-            assert!(result.is_ok());
-        }
+    #[test]
+    fn test_builder_accept_invalid_certs() {
+        let result = HttpClientBuilder::default()
+            .danger_accept_invalid_certs(true)
+            .build();
+        assert!(result.is_ok());
+    }
 
-        #[test]
-        fn test_builder_proxy() {
-            let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
-            let result = HttpClientBuilder::default().proxy(proxy_url).build();
-            assert!(result.is_ok());
-        }
+    #[test]
+    fn test_builder_accept_invalid_certs_false() {
+        let result = HttpClientBuilder::default()
+            .danger_accept_invalid_certs(false)
+            .build();
+        assert!(result.is_ok());
+    }
 
-        #[test]
-        fn test_builder_proxy_with_valid_matcher() {
-            let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
-            let result =
-                HttpClientBuilder::default().proxy_with_matcher(proxy_url, r".*\.example\.com$");
-            assert!(result.is_ok());
+    #[test]
+    fn test_builder_proxy() {
+        let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+        let result = HttpClientBuilder::default().proxy(proxy_url).build();
+        assert!(result.is_ok());
+    }
 
-            let builder = result.expect("Valid matcher should succeed");
-            let client_result = builder.build();
-            assert!(client_result.is_ok());
-        }
+    #[test]
+    fn test_builder_proxy_with_valid_matcher() {
+        let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+        let result =
+            HttpClientBuilder::default().proxy_with_matcher(proxy_url, r".*\.example\.com$");
+        assert!(result.is_ok());
 
-        #[test]
-        fn test_builder_proxy_with_invalid_matcher() {
-            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");
-            assert!(result.is_err());
+        let builder = result.expect("Valid matcher should succeed");
+        let client_result = builder.build();
+        assert!(client_result.is_ok());
+    }
 
-            if let Err(HttpError::Proxy(msg)) = result {
-                assert!(msg.contains("Invalid proxy pattern"));
-            } else {
-                panic!("Expected HttpError::Proxy");
-            }
+    #[test]
+    fn test_builder_proxy_with_invalid_matcher() {
+        let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+        let result = HttpClientBuilder::default().proxy_with_matcher(proxy_url, r"[invalid");
+        assert!(result.is_err());
+
+        if let Err(HttpError::Proxy(msg)) = result {
+            assert!(msg.contains("Invalid proxy pattern"));
+        } else {
+            panic!("Expected HttpError::Proxy");
         }
+    }
 
-        #[test]
-        fn test_builder_chained_config() {
-            let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
-            let result = HttpClientBuilder::default()
-                .danger_accept_invalid_certs(true)
-                .proxy(proxy_url)
-                .build();
-            assert!(result.is_ok());
-        }
+    #[test]
+    fn test_builder_chained_config() {
+        let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+        let result = HttpClientBuilder::default()
+            .danger_accept_invalid_certs(true)
+            .proxy(proxy_url)
+            .build();
+        assert!(result.is_ok());
     }
 }

+ 16 - 3
crates/cdk-http-client/src/error.rs

@@ -2,6 +2,10 @@
 
 use thiserror::Error;
 
+/// HTTP Response type - generic over the body type R and error type E
+/// This is the primary return type for all HTTP operations
+pub type Response<R, E = HttpError> = Result<R, E>;
+
 /// HTTP errors that can occur during requests
 #[derive(Debug, Error)]
 pub enum HttpError {
@@ -33,6 +37,7 @@ pub enum HttpError {
     Other(String),
 }
 
+#[cfg(not(target_arch = "wasm32"))]
 impl From<reqwest::Error> for HttpError {
     fn from(err: reqwest::Error) -> Self {
         if err.is_timeout() {
@@ -45,8 +50,6 @@ impl From<reqwest::Error> for HttpError {
                 message: err.to_string(),
             }
         } else {
-            // is_connect() is not available on wasm32
-            #[cfg(not(target_arch = "wasm32"))]
             if err.is_connect() {
                 return HttpError::Connection(err.to_string());
             }
@@ -112,7 +115,6 @@ mod tests {
 
     #[test]
     fn test_from_serde_json_error() {
-        // Create an invalid JSON parse to get a serde_json::Error
         let result: Result<String, _> = serde_json::from_str("not valid json");
         let json_error = result.expect_err("Invalid JSON should produce an error");
         let http_error: HttpError = json_error.into();
@@ -127,4 +129,15 @@ mod tests {
             _ => panic!("Expected HttpError::Serialization"),
         }
     }
+
+    #[test]
+    fn test_response_type_is_result() {
+        let success: Response<i32> = Ok(42);
+        assert!(success.is_ok());
+        assert!(matches!(success, Ok(42)));
+
+        let error: Response<i32> = Err(HttpError::Timeout);
+        assert!(error.is_err());
+        assert!(matches!(error, Err(HttpError::Timeout)));
+    }
 }

+ 22 - 5
crates/cdk-http-client/src/lib.rs

@@ -1,7 +1,7 @@
 //! HTTP client abstraction for CDK
 //!
-//! This crate provides an HTTP client wrapper that abstracts the underlying HTTP library (reqwest).
-//! Using this crate allows other CDK crates to avoid direct dependencies on reqwest.
+//! This crate provides an HTTP client wrapper that abstracts the underlying HTTP library.
+//! On native targets it uses reqwest; on WASM it calls the browser's `fetch()` API directly.
 //!
 //! # Example
 //!
@@ -20,12 +20,29 @@
 //! }
 //! ```
 
-mod client;
 mod error;
+
+#[cfg(not(target_arch = "wasm32"))]
+mod client;
+#[cfg(not(target_arch = "wasm32"))]
 mod request;
+#[cfg(not(target_arch = "wasm32"))]
 mod response;
 
+#[cfg(target_arch = "wasm32")]
+mod wasm;
+
+// Shared
+pub use error::{HttpError, Response};
+
+// Native re-exports
+#[cfg(not(target_arch = "wasm32"))]
 pub use client::{fetch, HttpClient, HttpClientBuilder};
-pub use error::HttpError;
+#[cfg(not(target_arch = "wasm32"))]
 pub use request::RequestBuilder;
-pub use response::{RawResponse, Response};
+#[cfg(not(target_arch = "wasm32"))]
+pub use response::RawResponse;
+
+// WASM re-exports
+#[cfg(target_arch = "wasm32")]
+pub use wasm::{fetch, HttpClient, HttpClientBuilder, RawResponse, RequestBuilder};

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

@@ -4,7 +4,8 @@ use serde::de::DeserializeOwned;
 use serde::Serialize;
 
 use crate::error::HttpError;
-use crate::response::{RawResponse, Response};
+use crate::response::RawResponse;
+use crate::Response;
 
 /// HTTP request builder for complex requests
 #[derive(Debug)]
@@ -13,7 +14,6 @@ pub struct RequestBuilder {
 }
 
 impl RequestBuilder {
-    /// Create a new RequestBuilder from a reqwest::RequestBuilder
     pub(crate) fn new(inner: reqwest::RequestBuilder) -> Self {
         Self { inner }
     }

+ 1 - 25
crates/cdk-http-client/src/response.rs

@@ -3,10 +3,7 @@
 use serde::de::DeserializeOwned;
 
 use crate::error::HttpError;
-
-/// HTTP Response type - generic over the body type R and error type E
-/// This is the primary return type for all HTTP operations
-pub type Response<R, E = HttpError> = Result<R, E>;
+use crate::Response;
 
 /// Raw HTTP response with status code and body access
 #[derive(Debug)]
@@ -16,7 +13,6 @@ pub struct 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(),
@@ -63,23 +59,3 @@ impl RawResponse {
             .map_err(HttpError::from)
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    // Note: RawResponse tests require a real reqwest::Response,
-    // so they are in tests/integration.rs using mockito.
-
-    #[test]
-    fn test_response_type_is_result() {
-        // Response<R, E> is just a type alias for Result<R, E>
-        let success: Response<i32> = Ok(42);
-        assert!(success.is_ok());
-        assert!(matches!(success, Ok(42)));
-
-        let error: Response<i32> = Err(HttpError::Timeout);
-        assert!(error.is_err());
-        assert!(matches!(error, Err(HttpError::Timeout)));
-    }
-}

+ 107 - 0
crates/cdk-http-client/src/wasm/client.rs

@@ -0,0 +1,107 @@
+//! WASM HTTP client using the browser's native `fetch()` API
+
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+
+use crate::wasm::request::RequestBuilder;
+use crate::wasm::response::RawResponse;
+use crate::Response;
+
+/// HTTP client wrapper
+#[derive(Debug, Clone)]
+pub struct HttpClient;
+
+impl Default for HttpClient {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+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()
+    }
+
+    // === Simple convenience methods ===
+
+    /// GET request, returns JSON deserialized to R
+    pub async fn fetch<R>(&self, url: &str) -> Response<R>
+    where
+        R: DeserializeOwned,
+    {
+        self.get(url).send_json().await
+    }
+
+    /// POST with JSON body, returns JSON deserialized to R
+    pub async fn post_json<B, R>(&self, url: &str, body: &B) -> Response<R>
+    where
+        B: Serialize + ?Sized,
+        R: DeserializeOwned,
+    {
+        self.post(url).json(body).send_json().await
+    }
+
+    /// POST with form data, returns JSON deserialized to R
+    pub async fn post_form<F, R>(&self, url: &str, form: &F) -> Response<R>
+    where
+        F: Serialize + ?Sized,
+        R: DeserializeOwned,
+    {
+        self.post(url).form(form).send_json().await
+    }
+
+    /// PATCH with JSON body, returns JSON deserialized to R
+    pub async fn patch_json<B, R>(&self, url: &str, body: &B) -> Response<R>
+    where
+        B: Serialize + ?Sized,
+        R: DeserializeOwned,
+    {
+        self.patch(url).json(body).send_json().await
+    }
+
+    // === Raw request methods ===
+
+    /// GET request returning raw response body
+    pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
+        self.get(url).send().await
+    }
+
+    // === Request builder methods ===
+
+    /// POST request builder for complex cases (custom headers, form data, etc.)
+    pub fn post(&self, url: &str) -> RequestBuilder {
+        RequestBuilder::new("POST", url)
+    }
+
+    /// GET request builder for complex cases (custom headers, etc.)
+    pub fn get(&self, url: &str) -> RequestBuilder {
+        RequestBuilder::new("GET", url)
+    }
+
+    /// PATCH request builder for complex cases (custom headers, JSON body, etc.)
+    pub fn patch(&self, url: &str) -> RequestBuilder {
+        RequestBuilder::new("PATCH", url)
+    }
+}
+
+/// HTTP client builder for configuring proxy and TLS settings
+#[derive(Debug, Default)]
+pub struct HttpClientBuilder;
+
+impl HttpClientBuilder {
+    /// Build the HTTP client
+    pub fn build(self) -> Response<HttpClient> {
+        Ok(HttpClient::new())
+    }
+}
+
+/// Convenience function for simple GET requests
+pub async fn fetch<R: DeserializeOwned>(url: &str) -> Response<R> {
+    HttpClient::new().fetch(url).await
+}

+ 25 - 0
crates/cdk-http-client/src/wasm/mod.rs

@@ -0,0 +1,25 @@
+//! WASM implementation using the browser's native `fetch()` API
+
+mod client;
+mod request;
+mod response;
+
+pub use client::{fetch, HttpClient, HttpClientBuilder};
+pub use request::RequestBuilder;
+pub use response::RawResponse;
+
+use crate::error::HttpError;
+
+impl From<wasm_bindgen::JsValue> for HttpError {
+    fn from(err: wasm_bindgen::JsValue) -> Self {
+        let message = err
+            .as_string()
+            .or_else(|| {
+                js_sys::Reflect::get(&err, &"message".into())
+                    .ok()
+                    .and_then(|v| v.as_string())
+            })
+            .unwrap_or_else(|| format!("{:?}", err));
+        HttpError::Other(message)
+    }
+}

+ 132 - 0
crates/cdk-http-client/src/wasm/request.rs

@@ -0,0 +1,132 @@
+//! WASM HTTP request builder using the browser's native `fetch()` API
+
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+
+use crate::error::HttpError;
+use crate::wasm::response::RawResponse;
+use crate::Response;
+
+#[wasm_bindgen::prelude::wasm_bindgen]
+extern "C" {
+    #[wasm_bindgen::prelude::wasm_bindgen(js_name = "fetch")]
+    fn js_fetch(input: &web_sys::Request) -> js_sys::Promise;
+}
+
+/// HTTP request builder for complex requests
+pub struct RequestBuilder {
+    url: String,
+    method: String,
+    headers: Vec<(String, String)>,
+    body: Option<String>,
+}
+
+impl core::fmt::Debug for RequestBuilder {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.debug_struct("RequestBuilder")
+            .field("url", &self.url)
+            .field("method", &self.method)
+            .finish()
+    }
+}
+
+impl RequestBuilder {
+    pub(crate) fn new(method: &str, url: &str) -> Self {
+        Self {
+            url: url.to_string(),
+            method: method.to_string(),
+            headers: Vec::new(),
+            body: None,
+        }
+    }
+
+    /// Add a header to the request
+    pub 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
+    }
+
+    /// Set the request body as JSON
+    pub fn json<T: Serialize + ?Sized>(mut self, body: &T) -> Self {
+        match serde_json::to_string(body) {
+            Ok(json) => {
+                self.body = Some(json);
+                self.headers.push((
+                    "Content-Type".to_string(),
+                    "application/json".to_string(),
+                ));
+            }
+            Err(_) => {
+                // Body serialization failed; send() will produce a request without body.
+                // This matches reqwest's deferred-error behaviour.
+            }
+        }
+        self
+    }
+
+    /// Set the request body as form data
+    pub fn form<T: Serialize + ?Sized>(mut self, body: &T) -> Self {
+        if let Ok(serde_json::Value::Object(map)) = serde_json::to_value(body) {
+            let pairs: Vec<String> = map
+                .iter()
+                .map(|(k, v)| {
+                    let val_str = match v {
+                        serde_json::Value::String(s) => s.clone(),
+                        other => other.to_string(),
+                    };
+                    let k_enc = js_sys::encode_uri_component(k);
+                    let v_enc = js_sys::encode_uri_component(&val_str);
+                    format!("{}={}", String::from(k_enc), String::from(v_enc))
+                })
+                .collect();
+            self.body = Some(pairs.join("&"));
+            self.headers.push((
+                "Content-Type".to_string(),
+                "application/x-www-form-urlencoded".to_string(),
+            ));
+        }
+        self
+    }
+
+    /// Send the request and return a raw response
+    pub async fn send(self) -> Response<RawResponse> {
+        let init = web_sys::RequestInit::new();
+        init.set_method(&self.method);
+
+        if let Some(ref body) = self.body {
+            init.set_body(&wasm_bindgen::JsValue::from_str(body));
+        }
+
+        let request =
+            web_sys::Request::new_with_str_and_init(&self.url, &init).map_err(HttpError::from)?;
+
+        for (key, value) in &self.headers {
+            request
+                .headers()
+                .set(key, value)
+                .map_err(HttpError::from)?;
+        }
+
+        let promise = js_fetch(&request);
+        let js_value = wasm_bindgen_futures::JsFuture::from(promise)
+            .await
+            .map_err(HttpError::from)?;
+
+        let response: web_sys::Response = js_value.into();
+        Ok(RawResponse::new(response))
+    }
+
+    /// Send the request and deserialize the response as JSON
+    pub async fn send_json<R: DeserializeOwned>(self) -> Response<R> {
+        let raw = self.send().await?;
+        let status = raw.status();
+
+        if !(200..300).contains(&status) {
+            let message = raw.text().await.unwrap_or_default();
+            return Err(HttpError::Status { status, message });
+        }
+
+        raw.json().await
+    }
+}

+ 74 - 0
crates/cdk-http-client/src/wasm/response.rs

@@ -0,0 +1,74 @@
+//! WASM HTTP response wrapping `web_sys::Response`
+
+use serde::de::DeserializeOwned;
+
+use crate::error::HttpError;
+use crate::Response;
+
+/// Raw HTTP response with status code and body access
+pub struct RawResponse {
+    status: u16,
+    inner: web_sys::Response,
+}
+
+impl core::fmt::Debug for RawResponse {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.debug_struct("RawResponse")
+            .field("status", &self.status)
+            .finish()
+    }
+}
+
+impl RawResponse {
+    pub(crate) fn new(response: web_sys::Response) -> Self {
+        let status = response.status();
+        Self { status, inner: response }
+    }
+
+    /// Get the HTTP status code
+    pub fn status(&self) -> u16 {
+        self.status
+    }
+
+    /// Check if the response status is a success (2xx)
+    pub fn is_success(&self) -> bool {
+        (200..300).contains(&self.status)
+    }
+
+    /// Check if the response status is a client error (4xx)
+    pub fn is_client_error(&self) -> bool {
+        (400..500).contains(&self.status)
+    }
+
+    /// Check if the response status is a server error (5xx)
+    pub fn is_server_error(&self) -> bool {
+        (500..600).contains(&self.status)
+    }
+
+    /// Get the response body as text
+    pub async fn text(self) -> Response<String> {
+        let promise = self.inner.text().map_err(HttpError::from)?;
+        let js_value = wasm_bindgen_futures::JsFuture::from(promise)
+            .await
+            .map_err(HttpError::from)?;
+        js_value
+            .as_string()
+            .ok_or_else(|| HttpError::Other("Response body is not a string".into()))
+    }
+
+    /// Get the response body as JSON
+    pub async fn json<T: DeserializeOwned>(self) -> Response<T> {
+        let text = self.text().await?;
+        serde_json::from_str(&text).map_err(HttpError::from)
+    }
+
+    /// Get the response body as bytes
+    pub async fn bytes(self) -> Response<Vec<u8>> {
+        let promise = self.inner.array_buffer().map_err(HttpError::from)?;
+        let js_value = wasm_bindgen_futures::JsFuture::from(promise)
+            .await
+            .map_err(HttpError::from)?;
+        let array = js_sys::Uint8Array::new(&js_value);
+        Ok(array.to_vec())
+    }
+}

+ 1 - 1
crates/cdk-signatory/Cargo.toml

@@ -26,9 +26,9 @@ tonic = { workspace = true, optional = true, features = ["transport", "tls-ring"
 tonic-prost = { workspace = true, optional = true }
 prost = { workspace = true, optional = true }
 tracing.workspace = true
-rustls = { workspace = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+rustls = { workspace = true }
 # main.rs dependencies
 anyhow.workspace = true
 cdk-sqlite = { workspace = true, features = ["mint"], optional = true }

+ 3 - 4
crates/cdk/Cargo.toml

@@ -12,22 +12,23 @@ license.workspace = true
 
 [features]
 default = ["mint", "wallet", "nostr", "bip353"]
-wallet = ["dep:futures", "cdk-common/wallet", "cdk-common/http", "dep:rustls"]
+wallet = ["dep:futures", "cdk-common/wallet", "cdk-common/http"]
 nostr = ["wallet", "dep:nostr-sdk", "cdk-common/nostr"]
 npubcash = ["wallet", "nostr", "dep:cdk-npubcash"]
 mint = ["dep:futures", "cdk-common/mint", "cdk-common/http", "cdk-signatory"]
 bip353 = ["dep:hickory-resolver"]
 # We do not commit to a MSRV with swagger enabled
 swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
+rustls = ["dep:rustls"]
 bench = []
 http_subscription = []
 tor = [
     "wallet",
+    "rustls",
     "dep:arti-client",
     "dep:arti-hyper",
     "dep:hyper",
     "dep:http",
-    "dep:rustls",
     "dep:tor-rtcompat",
     "dep:tls-api",
     "dep:tls-api-native-tls",
@@ -89,9 +90,7 @@ tls-api-native-tls = { version = "0.9", optional = true }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
-cdk-signatory = { workspace = true, default-features = false }
 getrandom = { version = "0.2", features = ["js"] }
-rustls = { workspace = true, optional = true }
 
 uuid = { workspace = true, features = ["js"] }
 gloo-timers = { version = "0.3", features = ["futures"] }

+ 1 - 1
crates/cdk/src/wallet/mint_connector/transport.rs

@@ -62,7 +62,7 @@ pub struct Async {
 
 impl Default for Async {
     fn default() -> Self {
-        #[cfg(not(target_arch = "wasm32"))]
+        #[cfg(all(not(target_arch = "wasm32"), feature = "rustls"))]
         if rustls::crypto::CryptoProvider::get_default().is_none() {
             let _ = rustls::crypto::ring::default_provider().install_default();
         }