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