reqwest_h3/wasm/
client.rs

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