Skip to main content

slumber_reqwest/wasm/
client.rs

1use http::header::USER_AGENT;
2use http::{HeaderMap, HeaderValue, Method};
3use js_sys::Promise;
4use std::convert::TryInto;
5use std::{fmt, future::Future, sync::Arc};
6use url::Url;
7use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _};
8use wasm_bindgen::JsCast;
9
10use super::{AbortGuard, Request, RequestBuilder, Response};
11use crate::IntoUrl;
12
13#[wasm_bindgen]
14extern "C" {
15    #[wasm_bindgen(js_name = fetch)]
16    fn fetch_with_request(input: &web_sys::Request) -> Promise;
17}
18
19fn js_fetch(req: &web_sys::Request) -> Promise {
20    use wasm_bindgen::{JsCast, JsValue};
21    let global = js_sys::global();
22
23    if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
24    {
25        global
26            .unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
27            .fetch_with_request(req)
28    } else {
29        // browser
30        fetch_with_request(req)
31    }
32}
33
34/// An HTTP Client for WebAssembly.
35///
36/// Uses the browser's Fetch API to make requests. The `Client` holds
37/// configuration that applies to all requests. To configure a `Client`,
38/// use `Client::builder()`.
39#[derive(Clone)]
40pub struct Client {
41    config: Arc<Config>,
42}
43
44/// A `ClientBuilder` can be used to create a `Client` with custom configuration.
45pub struct ClientBuilder {
46    config: Config,
47}
48
49impl Client {
50    /// Constructs a new `Client`.
51    pub fn new() -> Self {
52        Client::builder().build().unwrap_throw()
53    }
54
55    /// Creates a `ClientBuilder` to configure a `Client`.
56    ///
57    /// This is the same as `ClientBuilder::new()`.
58    pub fn builder() -> ClientBuilder {
59        ClientBuilder::new()
60    }
61
62    /// Convenience method to make a `GET` request to a URL.
63    ///
64    /// # Errors
65    ///
66    /// This method fails whenever supplied `Url` cannot be parsed.
67    pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
68        self.request(Method::GET, url)
69    }
70
71    /// Convenience method to make a `POST` request to a URL.
72    ///
73    /// # Errors
74    ///
75    /// This method fails whenever supplied `Url` cannot be parsed.
76    pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
77        self.request(Method::POST, url)
78    }
79
80    /// Convenience method to make a `PUT` request to a URL.
81    ///
82    /// # Errors
83    ///
84    /// This method fails whenever supplied `Url` cannot be parsed.
85    pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
86        self.request(Method::PUT, url)
87    }
88
89    /// Convenience method to make a `PATCH` request to a URL.
90    ///
91    /// # Errors
92    ///
93    /// This method fails whenever supplied `Url` cannot be parsed.
94    pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
95        self.request(Method::PATCH, url)
96    }
97
98    /// Convenience method to make a `DELETE` request to a URL.
99    ///
100    /// # Errors
101    ///
102    /// This method fails whenever supplied `Url` cannot be parsed.
103    pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
104        self.request(Method::DELETE, url)
105    }
106
107    /// Convenience method to make a `HEAD` request to a URL.
108    ///
109    /// # Errors
110    ///
111    /// This method fails whenever supplied `Url` cannot be parsed.
112    pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
113        self.request(Method::HEAD, url)
114    }
115
116    /// Start building a `Request` with the `Method` and `Url`.
117    ///
118    /// Returns a `RequestBuilder`, which will allow setting headers and
119    /// request body before sending.
120    ///
121    /// # Errors
122    ///
123    /// This method fails whenever supplied `Url` cannot be parsed.
124    pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
125        let req = url.into_url().map(move |url| Request::new(method, url));
126        RequestBuilder::new(self.clone(), req)
127    }
128
129    /// Executes a `Request`.
130    ///
131    /// A `Request` can be built manually with `Request::new()` or obtained
132    /// from a RequestBuilder with `RequestBuilder::build()`.
133    ///
134    /// You should prefer to use the `RequestBuilder` and
135    /// `RequestBuilder::send()`.
136    ///
137    /// # Errors
138    ///
139    /// This method fails if there was an error while sending request,
140    /// redirect loop was detected or redirect limit was exhausted.
141    pub fn execute(
142        &self,
143        request: Request,
144    ) -> impl Future<Output = Result<Response, crate::Error>> {
145        self.execute_request(request)
146    }
147
148    // merge request headers with Client default_headers, prior to external http fetch
149    fn merge_headers(&self, req: &mut Request) {
150        use http::header::Entry;
151        let headers: &mut HeaderMap = req.headers_mut();
152        // insert default headers in the request headers
153        // without overwriting already appended headers.
154        for (key, value) in self.config.headers.iter() {
155            if let Entry::Vacant(entry) = headers.entry(key) {
156                entry.insert(value.clone());
157            }
158        }
159    }
160
161    pub(super) fn execute_request(
162        &self,
163        mut req: Request,
164    ) -> impl Future<Output = crate::Result<Response>> {
165        self.merge_headers(&mut req);
166        fetch(req)
167    }
168}
169
170impl Default for Client {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176impl fmt::Debug for Client {
177    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178        let mut builder = f.debug_struct("Client");
179        self.config.fmt_fields(&mut builder);
180        builder.finish()
181    }
182}
183
184impl fmt::Debug for ClientBuilder {
185    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
186        let mut builder = f.debug_struct("ClientBuilder");
187        self.config.fmt_fields(&mut builder);
188        builder.finish()
189    }
190}
191
192// Can use new methods in web-sys when requiring v0.2.93.
193// > `init.method(m)` to `init.set_method(m)`
194// For now, ignore their deprecation.
195#[allow(deprecated)]
196async fn fetch(mut req: Request) -> crate::Result<Response> {
197    // Build the js Request
198    let mut init = web_sys::RequestInit::new();
199    init.method(req.method().as_str());
200
201    // convert HeaderMap to Headers
202    let js_headers = web_sys::Headers::new()
203        .map_err(crate::error::wasm)
204        .map_err(crate::error::builder)?;
205
206    for (name, value) in req.headers() {
207        js_headers
208            .append(
209                name.as_str(),
210                value.to_str().map_err(crate::error::builder)?,
211            )
212            .map_err(crate::error::wasm)
213            .map_err(crate::error::builder)?;
214    }
215    init.headers(&js_headers.into());
216
217    // When req.cors is true, do nothing because the default mode is 'cors'
218    if !req.cors {
219        init.mode(web_sys::RequestMode::NoCors);
220    }
221
222    if let Some(creds) = req.credentials {
223        init.credentials(creds);
224    }
225
226    if let Some(cache) = req.cache {
227        init.set_cache(cache);
228    }
229
230    if let Some(duplex) = req.duplex() {
231        js_sys::Reflect::set(
232            &js_sys::Object::from(wasm_bindgen::JsValue::from(&init)),
233            &wasm_bindgen::JsValue::from_str("duplex"),
234            &wasm_bindgen::JsValue::from_str(&duplex),
235        )
236        .expect_throw("failed to set duplex");
237    }
238
239    if let Some(body) = req.body_mut().take() {
240        if !body.is_empty() {
241            init.body(Some(body.into_js_value()?.as_ref()));
242        }
243    }
244
245    let mut abort = AbortGuard::new()?;
246    if let Some(timeout) = req.timeout() {
247        abort.timeout(*timeout);
248    }
249    init.signal(Some(&abort.signal()));
250
251    let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
252        .map_err(crate::error::wasm)
253        .map_err(crate::error::builder)?;
254
255    // Await the fetch() promise
256    let p = js_fetch(&js_req);
257    let js_resp = super::promise::<web_sys::Response>(p)
258        .await
259        .map_err(|error| {
260            if error.to_string() == "JsValue(\"reqwest::errors::TimedOut\")" {
261                crate::error::TimedOut.into()
262            } else {
263                error
264            }
265        })
266        .map_err(crate::error::request)?;
267
268    // Convert from the js Response
269    let mut resp = http::Response::builder().status(js_resp.status());
270
271    let url = Url::parse(&js_resp.url()).expect_throw("url parse");
272
273    let js_headers = js_resp.headers();
274    for item in js_headers.entries() {
275        let item = item.expect_throw("headers iterator doesn't throw");
276        let item: js_sys::Array = item.dyn_into().expect_throw("header item is an array");
277
278        let name = item
279            .get(0)
280            .as_string()
281            .expect_throw("header name is a string");
282
283        let value = item
284            .get(1)
285            .as_string()
286            .expect_throw("header value is a string");
287
288        resp = resp.header(&name, &value);
289    }
290
291    resp.body(js_resp)
292        .map(|resp| Response::new(resp, url, abort))
293        .map_err(crate::error::request)
294}
295
296// ===== impl ClientBuilder =====
297
298impl ClientBuilder {
299    /// Constructs a new `ClientBuilder`.
300    ///
301    /// This is the same as `Client::builder()`.
302    pub fn new() -> Self {
303        ClientBuilder {
304            config: Config::default(),
305        }
306    }
307
308    /// Returns a 'Client' that uses this ClientBuilder configuration
309    pub fn build(mut self) -> Result<Client, crate::Error> {
310        if let Some(err) = self.config.error {
311            return Err(err);
312        }
313
314        let config = std::mem::take(&mut self.config);
315        Ok(Client {
316            config: Arc::new(config),
317        })
318    }
319
320    /// Sets the `User-Agent` header to be used by this client.
321    pub fn user_agent<V>(mut self, value: V) -> ClientBuilder
322    where
323        V: TryInto<HeaderValue>,
324        V::Error: Into<http::Error>,
325    {
326        match value.try_into() {
327            Ok(value) => {
328                self.config.headers.insert(USER_AGENT, value);
329            }
330            Err(e) => {
331                self.config.error = Some(crate::error::builder(e.into()));
332            }
333        }
334        self
335    }
336
337    /// Sets the default headers for every request
338    pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
339        for (key, value) in headers.iter() {
340            self.config.headers.insert(key, value.clone());
341        }
342        self
343    }
344}
345
346impl Default for ClientBuilder {
347    fn default() -> Self {
348        Self::new()
349    }
350}
351
352#[derive(Debug)]
353struct Config {
354    headers: HeaderMap,
355    error: Option<crate::Error>,
356}
357
358impl Default for Config {
359    fn default() -> Config {
360        Config {
361            headers: HeaderMap::new(),
362            error: None,
363        }
364    }
365}
366
367impl Config {
368    fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
369        f.field("default_headers", &self.headers);
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use wasm_bindgen_test::*;
376
377    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
378
379    #[wasm_bindgen_test]
380    async fn default_headers() {
381        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
382
383        let mut headers = HeaderMap::new();
384        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
385        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
386        let client = crate::Client::builder()
387            .default_headers(headers)
388            .build()
389            .expect("client");
390        let mut req = client
391            .get("https://www.example.com")
392            .build()
393            .expect("request");
394        // merge headers as if client were about to issue fetch
395        client.merge_headers(&mut req);
396
397        let test_headers = req.headers();
398        assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
399        assert!(test_headers.get("x-custom").is_some(), "custom header");
400        assert!(test_headers.get("accept").is_none(), "no accept header");
401    }
402
403    #[wasm_bindgen_test]
404    async fn default_headers_clone() {
405        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
406
407        let mut headers = HeaderMap::new();
408        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
409        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
410        let client = crate::Client::builder()
411            .default_headers(headers)
412            .build()
413            .expect("client");
414
415        let mut req = client
416            .get("https://www.example.com")
417            .header(CONTENT_TYPE, "text/plain")
418            .build()
419            .expect("request");
420        client.merge_headers(&mut req);
421        let headers1 = req.headers();
422
423        // confirm that request headers override defaults
424        assert_eq!(
425            headers1.get(CONTENT_TYPE).unwrap(),
426            "text/plain",
427            "request headers override defaults"
428        );
429
430        // confirm that request headers don't change client defaults
431        let mut req2 = client
432            .get("https://www.example.com/x")
433            .build()
434            .expect("req 2");
435        client.merge_headers(&mut req2);
436        let headers2 = req2.headers();
437        assert_eq!(
438            headers2.get(CONTENT_TYPE).unwrap(),
439            "application/json",
440            "request headers don't change client defaults"
441        );
442    }
443
444    #[wasm_bindgen_test]
445    fn user_agent_header() {
446        use crate::header::USER_AGENT;
447
448        let client = crate::Client::builder()
449            .user_agent("FooBar/1.2.3")
450            .build()
451            .expect("client");
452
453        let mut req = client
454            .get("https://www.example.com")
455            .build()
456            .expect("request");
457
458        // Merge the client headers with the request's one.
459        client.merge_headers(&mut req);
460        let headers1 = req.headers();
461
462        // Confirm that we have the `User-Agent` header set
463        assert_eq!(
464            headers1.get(USER_AGENT).unwrap(),
465            "FooBar/1.2.3",
466            "The user-agent header was not set: {req:#?}"
467        );
468
469        // Now we try to overwrite the `User-Agent` value
470
471        let mut req2 = client
472            .get("https://www.example.com")
473            .header(USER_AGENT, "Another-User-Agent/42")
474            .build()
475            .expect("request 2");
476
477        client.merge_headers(&mut req2);
478        let headers2 = req2.headers();
479
480        assert_eq!(
481            headers2.get(USER_AGENT).expect("headers2 user agent"),
482            "Another-User-Agent/42",
483            "Was not able to overwrite the User-Agent value on the request-builder"
484        );
485    }
486}