fetch_happen/
lib.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::fmt;
5use wasm_bindgen::prelude::*;
6use wasm_bindgen_futures::JsFuture;
7use web_sys::{AbortSignal, Request as WebRequest, RequestInit, Response as WebResponse};
8
9pub use web_sys::{AbortController, RequestMode};
10
11pub type Result<T> = std::result::Result<T, Error>;
12
13/// Errors that can occur when making a request
14#[derive(Debug)]
15pub enum Error {
16    /// JavaScript error
17    JsError(JsValue),
18    /// HTTP error with status code
19    HttpError(u16, String),
20    /// JSON parsing error
21    JsonError(String),
22    /// Request was aborted
23    Aborted,
24}
25
26impl From<JsValue> for Error {
27    fn from(value: JsValue) -> Self {
28        Error::JsError(value)
29    }
30}
31
32impl From<serde_json::Error> for Error {
33    fn from(err: serde_json::Error) -> Self {
34        Error::JsonError(err.to_string())
35    }
36}
37
38impl fmt::Display for Error {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Error::JsError(e) => write!(f, "JavaScript error: {:?}", e),
42            Error::HttpError(status, msg) => write!(f, "HTTP error {}: {}", status, msg),
43            Error::JsonError(e) => write!(f, "JSON error: {}", e),
44            Error::Aborted => write!(f, "Request was aborted"),
45        }
46    }
47}
48
49impl std::error::Error for Error {}
50
51/// HTTP methods
52#[derive(Debug, Clone, Copy)]
53pub enum Method {
54    GET,
55    POST,
56    PUT,
57    DELETE,
58    PATCH,
59    HEAD,
60    OPTIONS,
61}
62
63impl Method {
64    fn as_str(&self) -> &'static str {
65        match self {
66            Method::GET => "GET",
67            Method::POST => "POST",
68            Method::PUT => "PUT",
69            Method::DELETE => "DELETE",
70            Method::PATCH => "PATCH",
71            Method::HEAD => "HEAD",
72            Method::OPTIONS => "OPTIONS",
73        }
74    }
75}
76
77/// A builder for HTTP requests
78pub struct RequestBuilder {
79    url: String,
80    method: Method,
81    headers: HashMap<String, String>,
82    body: Option<String>,
83    mode: RequestMode,
84    signal: Option<AbortSignal>,
85}
86
87impl RequestBuilder {
88    fn new(method: Method, url: impl Into<String>) -> Self {
89        Self {
90            url: url.into(),
91            method,
92            headers: HashMap::new(),
93            body: None,
94            mode: RequestMode::Cors,
95            signal: None,
96        }
97    }
98
99    /// Set a header
100    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
101        self.headers.insert(key.into(), value.into());
102        self
103    }
104
105    /// Set multiple headers
106    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
107        self.headers.extend(headers);
108        self
109    }
110
111    /// Set the request mode (Cors, NoCors, SameOrigin)
112    pub fn mode(mut self, mode: RequestMode) -> Self {
113        self.mode = mode;
114        self
115    }
116
117    /// Set the request body as a string
118    pub fn body(mut self, body: impl Into<String>) -> Self {
119        self.body = Some(body.into());
120        self
121    }
122
123    /// Set the request body as JSON
124    pub fn json<T: Serialize>(mut self, json: &T) -> Result<Self> {
125        let body = serde_json::to_string(json)?;
126        self.body = Some(body);
127        self.headers
128            .insert("Content-Type".to_string(), "application/json".to_string());
129        Ok(self)
130    }
131
132    /// Set an abort signal for the request
133    pub fn abort_signal(mut self, signal: AbortSignal) -> Self {
134        self.signal = Some(signal);
135        self
136    }
137
138    /// Send the request and get a Response
139    pub async fn send(self) -> Result<Response> {
140        let opts = RequestInit::new();
141        opts.set_method(self.method.as_str());
142        opts.set_mode(self.mode);
143
144        if let Some(body) = &self.body {
145            opts.set_body(&JsValue::from_str(body));
146        }
147
148        if let Some(signal) = &self.signal {
149            opts.set_signal(Some(signal));
150        }
151
152        let request = WebRequest::new_with_str_and_init(&self.url, &opts)?;
153        let headers = request.headers();
154
155        for (key, value) in &self.headers {
156            headers.set(key, value)?;
157        }
158
159        let window = web_sys::window()
160            .ok_or_else(|| Error::JsError(JsValue::from_str("Failed to get window")))?;
161
162        let resp_value = JsFuture::from(window.fetch_with_request(&request))
163            .await
164            .map_err(|e| {
165                // Check if this is an abort error
166                if let Some(error) = e.dyn_ref::<js_sys::Error>() {
167                    if error.name() == "AbortError" {
168                        return Error::Aborted;
169                    }
170                }
171                Error::JsError(e)
172            })?;
173        let web_response: WebResponse = resp_value
174            .dyn_into()
175            .map_err(|_| Error::JsError(JsValue::from_str("Response conversion failed")))?;
176
177        Ok(Response::from_web_response(web_response))
178    }
179}
180
181/// A response from a fetch request
182pub struct Response {
183    inner: WebResponse,
184}
185
186impl Response {
187    fn from_web_response(response: WebResponse) -> Self {
188        Self { inner: response }
189    }
190
191    /// Get the status code
192    pub fn status(&self) -> u16 {
193        self.inner.status()
194    }
195
196    /// Check if the response was successful (status 200-299)
197    pub fn ok(&self) -> bool {
198        self.inner.ok()
199    }
200
201    /// Get a header value
202    pub fn header(&self, name: &str) -> Result<Option<String>> {
203        Ok(self.inner.headers().get(name)?)
204    }
205
206    /// Get the response body as text
207    pub async fn text(&self) -> Result<String> {
208        let promise = self.inner.text().map_err(Error::JsError)?;
209        let text = JsFuture::from(promise).await?;
210
211        text.as_string()
212            .ok_or_else(|| Error::JsError(JsValue::from_str("Failed to convert to string")))
213    }
214
215    /// Get the response body as JSON
216    pub async fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
217        let text = self.text().await?;
218        Ok(serde_json::from_str(&text)?)
219    }
220
221    /// Get the response body as a dynamic JSON value
222    pub async fn json_value(&self) -> Result<Value> {
223        self.json().await
224    }
225
226    /// Get the response body as bytes
227    pub async fn bytes(&self) -> Result<Vec<u8>> {
228        let promise = self.inner.array_buffer().map_err(Error::JsError)?;
229        let array_buffer = JsFuture::from(promise).await?;
230        let uint8_array = js_sys::Uint8Array::new(&array_buffer);
231        Ok(uint8_array.to_vec())
232    }
233
234    /// Ensure the response was successful, returning an error if not
235    pub fn error_for_status(self) -> Result<Self> {
236        if self.ok() {
237            Ok(self)
238        } else {
239            let status = self.status();
240            let text = format!("HTTP Error {}", status);
241            Err(Error::HttpError(status, text))
242        }
243    }
244}
245
246/// Main client for making HTTP requests
247pub struct Client;
248
249impl Client {
250    /// Make a GET request
251    pub fn get(&self, url: impl Into<String>) -> RequestBuilder {
252        RequestBuilder::new(Method::GET, url)
253    }
254
255    /// Make a POST request
256    pub fn post(&self, url: impl Into<String>) -> RequestBuilder {
257        RequestBuilder::new(Method::POST, url)
258    }
259
260    /// Make a PUT request
261    pub fn put(&self, url: impl Into<String>) -> RequestBuilder {
262        RequestBuilder::new(Method::PUT, url)
263    }
264
265    /// Make a DELETE request
266    pub fn delete(&self, url: impl Into<String>) -> RequestBuilder {
267        RequestBuilder::new(Method::DELETE, url)
268    }
269
270    /// Make a PATCH request
271    pub fn patch(&self, url: impl Into<String>) -> RequestBuilder {
272        RequestBuilder::new(Method::PATCH, url)
273    }
274
275    /// Make a HEAD request
276    pub fn head(&self, url: impl Into<String>) -> RequestBuilder {
277        RequestBuilder::new(Method::HEAD, url)
278    }
279}
280
281/// Convenience function for making a GET request
282pub async fn get(url: impl Into<String>) -> Result<Response> {
283    Client.get(url).send().await
284}
285
286/// Convenience function for making a POST request with JSON body
287pub async fn post_json<T: Serialize>(url: impl Into<String>, json: &T) -> Result<Response> {
288    Client.post(url).json(json)?.send().await
289}