Skip to main content

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(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    if let Some(referrer) = &req.referrer {
202        init.set_referrer(referrer);
203    }
204
205    if let Some(policy) = &req.referrer_policy {
206        let policy = match policy.as_str() {
207            "no-referrer" => web_sys::ReferrerPolicy::NoReferrer,
208            "no-referrer-when-downgrade" => web_sys::ReferrerPolicy::NoReferrerWhenDowngrade,
209            "origin" => web_sys::ReferrerPolicy::Origin,
210            "origin-when-cross-origin" => web_sys::ReferrerPolicy::OriginWhenCrossOrigin,
211            "same-origin" => web_sys::ReferrerPolicy::SameOrigin,
212            "strict-origin" => web_sys::ReferrerPolicy::StrictOrigin,
213            "strict-origin-when-cross-origin" => {
214                web_sys::ReferrerPolicy::StrictOriginWhenCrossOrigin
215            }
216            "unsafe-url" => web_sys::ReferrerPolicy::UnsafeUrl,
217            _ => web_sys::ReferrerPolicy::None,
218        };
219        init.set_referrer_policy(policy);
220    }
221
222    // convert HeaderMap to Headers
223    let js_headers = web_sys::Headers::new()
224        .map_err(crate::error::wasm)
225        .map_err(crate::error::builder)?;
226
227    for (name, value) in req.headers() {
228        js_headers
229            .append(
230                name.as_str(),
231                value.to_str().map_err(crate::error::builder)?,
232            )
233            .map_err(crate::error::wasm)
234            .map_err(crate::error::builder)?;
235    }
236    init.headers(&js_headers.into());
237
238    // When req.cors is true, do nothing because the default mode is 'cors'
239    if !req.cors {
240        init.mode(web_sys::RequestMode::NoCors);
241    }
242
243    if let Some(creds) = req.credentials {
244        init.credentials(creds);
245    }
246
247    if let Some(cache) = req.cache {
248        init.set_cache(cache);
249    }
250
251    if let Some(body) = req.body() {
252        if !body.is_empty() {
253            init.body(Some(body.to_js_value()?.as_ref()));
254        }
255    }
256
257    let mut abort = AbortGuard::new()?;
258    if let Some(timeout) = req.timeout() {
259        abort.timeout(*timeout);
260    }
261    init.signal(Some(&abort.signal()));
262
263    let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
264        .map_err(crate::error::wasm)
265        .map_err(crate::error::builder)?;
266
267    // Await the fetch() promise
268    let p = js_fetch(&js_req);
269    let js_resp = super::promise::<web_sys::Response>(p)
270        .await
271        .map_err(|error| {
272            if error.to_string() == "JsValue(\"reqwest::errors::TimedOut\")" {
273                crate::error::TimedOut.into()
274            } else {
275                error
276            }
277        })
278        .map_err(crate::error::request)?;
279
280    // Convert from the js Response
281    let mut resp = http::Response::builder().status(js_resp.status());
282
283    let url = Url::parse(&js_resp.url()).expect_throw("url parse");
284
285    let js_headers = js_resp.headers();
286    for item in js_headers.entries() {
287        let item = item.expect_throw("headers iterator doesn't throw");
288        let item: js_sys::Array = item.dyn_into().expect_throw("header item is an array");
289
290        let name = item
291            .get(0)
292            .as_string()
293            .expect_throw("header name is a string");
294
295        let value = item
296            .get(1)
297            .as_string()
298            .expect_throw("header value is a string");
299
300        resp = resp.header(&name, &value);
301    }
302
303    resp.body(js_resp)
304        .map(|resp| Response::new(resp, url, abort))
305        .map_err(crate::error::request)
306}
307
308// ===== impl ClientBuilder =====
309
310impl ClientBuilder {
311    /// Constructs a new `ClientBuilder`.
312    ///
313    /// This is the same as `Client::builder()`.
314    pub fn new() -> Self {
315        ClientBuilder {
316            config: Config::default(),
317        }
318    }
319
320    /// Returns a 'Client' that uses this ClientBuilder configuration
321    pub fn build(mut self) -> Result<Client, crate::Error> {
322        if let Some(err) = self.config.error {
323            return Err(err);
324        }
325
326        let config = std::mem::take(&mut self.config);
327        Ok(Client {
328            config: Arc::new(config),
329        })
330    }
331
332    /// Sets the `User-Agent` header to be used by this client.
333    pub fn user_agent<V>(mut self, value: V) -> ClientBuilder
334    where
335        V: TryInto<HeaderValue>,
336        V::Error: Into<http::Error>,
337    {
338        match value.try_into() {
339            Ok(value) => {
340                self.config.headers.insert(USER_AGENT, value);
341            }
342            Err(e) => {
343                self.config.error = Some(crate::error::builder(e.into()));
344            }
345        }
346        self
347    }
348
349    /// Sets the default headers for every request
350    pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
351        for (key, value) in headers.iter() {
352            self.config.headers.insert(key, value.clone());
353        }
354        self
355    }
356}
357
358impl Default for ClientBuilder {
359    fn default() -> Self {
360        Self::new()
361    }
362}
363
364#[derive(Debug)]
365struct Config {
366    headers: HeaderMap,
367    error: Option<crate::Error>,
368}
369
370impl Default for Config {
371    fn default() -> Config {
372        Config {
373            headers: HeaderMap::new(),
374            error: None,
375        }
376    }
377}
378
379impl Config {
380    fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
381        f.field("default_headers", &self.headers);
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use wasm_bindgen_test::*;
388
389    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
390
391    #[wasm_bindgen_test]
392    async fn default_headers() {
393        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
394
395        let mut headers = HeaderMap::new();
396        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
397        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
398        let client = crate::Client::builder()
399            .default_headers(headers)
400            .build()
401            .expect("client");
402        let mut req = client
403            .get("https://www.example.com")
404            .build()
405            .expect("request");
406        // merge headers as if client were about to issue fetch
407        client.merge_headers(&mut req);
408
409        let test_headers = req.headers();
410        assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
411        assert!(test_headers.get("x-custom").is_some(), "custom header");
412        assert!(test_headers.get("accept").is_none(), "no accept header");
413    }
414
415    #[wasm_bindgen_test]
416    async fn default_headers_clone() {
417        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
418
419        let mut headers = HeaderMap::new();
420        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
421        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
422        let client = crate::Client::builder()
423            .default_headers(headers)
424            .build()
425            .expect("client");
426
427        let mut req = client
428            .get("https://www.example.com")
429            .header(CONTENT_TYPE, "text/plain")
430            .build()
431            .expect("request");
432        client.merge_headers(&mut req);
433        let headers1 = req.headers();
434
435        // confirm that request headers override defaults
436        assert_eq!(
437            headers1.get(CONTENT_TYPE).unwrap(),
438            "text/plain",
439            "request headers override defaults"
440        );
441
442        // confirm that request headers don't change client defaults
443        let mut req2 = client
444            .get("https://www.example.com/x")
445            .build()
446            .expect("req 2");
447        client.merge_headers(&mut req2);
448        let headers2 = req2.headers();
449        assert_eq!(
450            headers2.get(CONTENT_TYPE).unwrap(),
451            "application/json",
452            "request headers don't change client defaults"
453        );
454    }
455
456    #[wasm_bindgen_test]
457    fn user_agent_header() {
458        use crate::header::USER_AGENT;
459
460        let client = crate::Client::builder()
461            .user_agent("FooBar/1.2.3")
462            .build()
463            .expect("client");
464
465        let mut req = client
466            .get("https://www.example.com")
467            .build()
468            .expect("request");
469
470        // Merge the client headers with the request's one.
471        client.merge_headers(&mut req);
472        let headers1 = req.headers();
473
474        // Confirm that we have the `User-Agent` header set
475        assert_eq!(
476            headers1.get(USER_AGENT).unwrap(),
477            "FooBar/1.2.3",
478            "The user-agent header was not set: {req:#?}"
479        );
480
481        // Now we try to overwrite the `User-Agent` value
482
483        let mut req2 = client
484            .get("https://www.example.com")
485            .header(USER_AGENT, "Another-User-Agent/42")
486            .build()
487            .expect("request 2");
488
489        client.merge_headers(&mut req2);
490        let headers2 = req2.headers();
491
492        assert_eq!(
493            headers2.get(USER_AGENT).expect("headers2 user agent"),
494            "Another-User-Agent/42",
495            "Was not able to overwrite the User-Agent value on the request-builder"
496        );
497    }
498}