reqwest_wasm/wasm/
client.rs

1use http::{HeaderMap, Method};
2use js_sys::{Promise, JSON};
3use std::{fmt, future::Future, sync::Arc};
4use url::Url;
5use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _};
6
7use super::{Request, RequestBuilder, Response};
8use crate::IntoUrl;
9
10#[wasm_bindgen]
11extern "C" {
12    #[wasm_bindgen(js_name = fetch)]
13    fn fetch_with_request(input: &web_sys::Request) -> Promise;
14}
15
16fn js_fetch(req: &web_sys::Request) -> Promise {
17    use wasm_bindgen::{JsCast, JsValue};
18    let global = js_sys::global();
19
20    if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
21    {
22        global
23            .unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
24            .fetch_with_request(req)
25    } else {
26        // browser
27        fetch_with_request(req)
28    }
29}
30
31/// dox
32#[derive(Clone)]
33pub struct Client {
34    config: Arc<Config>,
35}
36
37/// dox
38pub struct ClientBuilder {
39    config: Config,
40}
41
42impl Client {
43    /// dox
44    pub fn new() -> Self {
45        Client::builder().build().unwrap_throw()
46    }
47
48    /// dox
49    pub fn builder() -> ClientBuilder {
50        ClientBuilder::new()
51    }
52
53    /// Convenience method to make a `GET` request to a URL.
54    ///
55    /// # Errors
56    ///
57    /// This method fails whenever supplied `Url` cannot be parsed.
58    pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
59        self.request(Method::GET, url)
60    }
61
62    /// Convenience method to make a `POST` request to a URL.
63    ///
64    /// # Errors
65    ///
66    /// This method fails whenever supplied `Url` cannot be parsed.
67    pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
68        self.request(Method::POST, url)
69    }
70
71    /// Convenience method to make a `PUT` request to a URL.
72    ///
73    /// # Errors
74    ///
75    /// This method fails whenever supplied `Url` cannot be parsed.
76    pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
77        self.request(Method::PUT, url)
78    }
79
80    /// Convenience method to make a `PATCH` request to a URL.
81    ///
82    /// # Errors
83    ///
84    /// This method fails whenever supplied `Url` cannot be parsed.
85    pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
86        self.request(Method::PATCH, url)
87    }
88
89    /// Convenience method to make a `DELETE` request to a URL.
90    ///
91    /// # Errors
92    ///
93    /// This method fails whenever supplied `Url` cannot be parsed.
94    pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
95        self.request(Method::DELETE, url)
96    }
97
98    /// Convenience method to make a `HEAD` request to a URL.
99    ///
100    /// # Errors
101    ///
102    /// This method fails whenever supplied `Url` cannot be parsed.
103    pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
104        self.request(Method::HEAD, url)
105    }
106
107    /// Start building a `Request` with the `Method` and `Url`.
108    ///
109    /// Returns a `RequestBuilder`, which will allow setting headers and
110    /// request body before sending.
111    ///
112    /// # Errors
113    ///
114    /// This method fails whenever supplied `Url` cannot be parsed.
115    pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
116        let req = url.into_url().map(move |url| Request::new(method, url));
117        RequestBuilder::new(self.clone(), req)
118    }
119
120    /// Executes a `Request`.
121    ///
122    /// A `Request` can be built manually with `Request::new()` or obtained
123    /// from a RequestBuilder with `RequestBuilder::build()`.
124    ///
125    /// You should prefer to use the `RequestBuilder` and
126    /// `RequestBuilder::send()`.
127    ///
128    /// # Errors
129    ///
130    /// This method fails if there was an error while sending request,
131    /// redirect loop was detected or redirect limit was exhausted.
132    pub fn execute(
133        &self,
134        request: Request,
135    ) -> impl Future<Output = Result<Response, crate::Error>> {
136        self.execute_request(request)
137    }
138
139    // merge request headers with Client default_headers, prior to external http fetch
140    fn merge_headers(&self, req: &mut Request) {
141        use http::header::Entry;
142        let headers: &mut HeaderMap = req.headers_mut();
143        // insert default headers in the request headers
144        // without overwriting already appended headers.
145        for (key, value) in self.config.headers.iter() {
146            if let Entry::Vacant(entry) = headers.entry(key) {
147                entry.insert(value.clone());
148            }
149        }
150    }
151
152    pub(super) fn execute_request(
153        &self,
154        mut req: Request,
155    ) -> impl Future<Output = crate::Result<Response>> {
156        self.merge_headers(&mut req);
157        fetch(req)
158    }
159}
160
161impl Default for Client {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl fmt::Debug for Client {
168    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
169        let mut builder = f.debug_struct("Client");
170        self.config.fmt_fields(&mut builder);
171        builder.finish()
172    }
173}
174
175impl fmt::Debug for ClientBuilder {
176    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177        let mut builder = f.debug_struct("ClientBuilder");
178        self.config.fmt_fields(&mut builder);
179        builder.finish()
180    }
181}
182
183async fn fetch(req: Request) -> crate::Result<Response> {
184    // Build the js Request
185    let mut init = web_sys::RequestInit::new();
186    init.method(req.method().as_str());
187
188    // convert HeaderMap to Headers
189    let js_headers = web_sys::Headers::new()
190        .map_err(crate::error::wasm)
191        .map_err(crate::error::builder)?;
192
193    for (name, value) in req.headers() {
194        js_headers
195            .append(
196                name.as_str(),
197                value.to_str().map_err(crate::error::builder)?,
198            )
199            .map_err(crate::error::wasm)
200            .map_err(crate::error::builder)?;
201    }
202    init.headers(&js_headers.into());
203
204    // When req.cors is true, do nothing because the default mode is 'cors'
205    if !req.cors {
206        init.mode(web_sys::RequestMode::NoCors);
207    }
208
209    if let Some(creds) = req.credentials {
210        init.credentials(creds);
211    }
212
213    if let Some(body) = req.body() {
214        if !body.is_empty() {
215            init.body(Some(body.to_js_value()?.as_ref()));
216        }
217    }
218
219    let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
220        .map_err(crate::error::wasm)
221        .map_err(crate::error::builder)?;
222
223    // Await the fetch() promise
224    let p = js_fetch(&js_req);
225    let js_resp = super::promise::<web_sys::Response>(p)
226        .await
227        .map_err(crate::error::request)?;
228
229    // Convert from the js Response
230    let mut resp = http::Response::builder().status(js_resp.status());
231
232    let url = Url::parse(&js_resp.url()).expect_throw("url parse");
233
234    let js_headers = js_resp.headers();
235    let js_iter = js_sys::try_iter(&js_headers)
236        .expect_throw("headers try_iter")
237        .expect_throw("headers have an iterator");
238
239    for item in js_iter {
240        let item = item.expect_throw("headers iterator doesn't throw");
241        let serialized_headers: String = JSON::stringify(&item)
242            .expect_throw("serialized headers")
243            .into();
244        let [name, value]: [String; 2] = serde_json::from_str(&serialized_headers)
245            .expect_throw("deserializable serialized headers");
246        resp = resp.header(&name, &value);
247    }
248
249    resp.body(js_resp)
250        .map(|resp| Response::new(resp, url))
251        .map_err(crate::error::request)
252}
253
254// ===== impl ClientBuilder =====
255
256impl ClientBuilder {
257    /// dox
258    pub fn new() -> Self {
259        ClientBuilder {
260            config: Config::default(),
261        }
262    }
263
264    /// Returns a 'Client' that uses this ClientBuilder configuration
265    pub fn build(mut self) -> Result<Client, crate::Error> {
266        let config = std::mem::take(&mut self.config);
267        Ok(Client {
268            config: Arc::new(config),
269        })
270    }
271
272    /// Sets the default headers for every request
273    pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
274        for (key, value) in headers.iter() {
275            self.config.headers.insert(key, value.clone());
276        }
277        self
278    }
279}
280
281impl Default for ClientBuilder {
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287#[derive(Clone, Debug)]
288struct Config {
289    headers: HeaderMap,
290}
291
292impl Default for Config {
293    fn default() -> Config {
294        Config {
295            headers: HeaderMap::new(),
296        }
297    }
298}
299
300impl Config {
301    fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
302        f.field("default_headers", &self.headers);
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use wasm_bindgen_test::*;
309
310    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
311
312    #[wasm_bindgen_test]
313    async fn default_headers() {
314        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
315
316        let mut headers = HeaderMap::new();
317        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
318        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
319        let client = crate::Client::builder()
320            .default_headers(headers)
321            .build()
322            .expect("client");
323        let mut req = client
324            .get("https://www.example.com")
325            .build()
326            .expect("request");
327        // merge headers as if client were about to issue fetch
328        client.merge_headers(&mut req);
329
330        let test_headers = req.headers();
331        assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
332        assert!(test_headers.get("x-custom").is_some(), "custom header");
333        assert!(test_headers.get("accept").is_none(), "no accept header");
334    }
335
336    #[wasm_bindgen_test]
337    async fn default_headers_clone() {
338        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
339
340        let mut headers = HeaderMap::new();
341        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
342        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
343        let client = crate::Client::builder()
344            .default_headers(headers)
345            .build()
346            .expect("client");
347
348        let mut req = client
349            .get("https://www.example.com")
350            .header(CONTENT_TYPE, "text/plain")
351            .build()
352            .expect("request");
353        client.merge_headers(&mut req);
354        let headers1 = req.headers();
355
356        // confirm that request headers override defaults
357        assert_eq!(
358            headers1.get(CONTENT_TYPE).unwrap(),
359            "text/plain",
360            "request headers override defaults"
361        );
362
363        // confirm that request headers don't change client defaults
364        let mut req2 = client
365            .get("https://www.example.com/x")
366            .build()
367            .expect("req 2");
368        client.merge_headers(&mut req2);
369        let headers2 = req2.headers();
370        assert_eq!(
371            headers2.get(CONTENT_TYPE).unwrap(),
372            "application/json",
373            "request headers don't change client defaults"
374        );
375    }
376}