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