yew_stdweb/services/fetch/
std_web.rs

1//! `stdweb` implementation for the fetch service.
2
3use super::Referrer;
4use crate::callback::Callback;
5use crate::format::{Binary, Format, Text};
6use crate::services::Task;
7use serde::Serialize;
8use std::collections::HashMap;
9use std::fmt;
10use stdweb::serde::Serde;
11use stdweb::unstable::{TryFrom, TryInto};
12use stdweb::web::error::Error;
13use stdweb::web::ArrayBuffer;
14use stdweb::{JsSerialize, Value};
15#[allow(unused_imports)]
16use stdweb::{_js_impl, js};
17use thiserror::Error;
18
19#[doc(no_inline)]
20pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};
21
22/// Type to set cache for fetch.
23#[derive(Serialize, Debug)]
24#[serde(rename_all = "kebab-case")]
25pub enum Cache {
26    /// `default` value of cache.
27    #[serde(rename = "default")]
28    DefaultCache,
29    /// `no-store` value of cache.
30    NoStore,
31    /// `reload` value of cache.
32    Reload,
33    /// `no-cache` value of cache.
34    NoCache,
35    /// `force-cache` value of cache
36    ForceCache,
37    /// `only-if-cached` value of cache
38    OnlyIfCached,
39}
40
41/// Type to set credentials for fetch.
42#[derive(Serialize, Debug)]
43#[serde(rename_all = "kebab-case")]
44pub enum Credentials {
45    /// `omit` value of credentials.
46    Omit,
47    /// `include` value of credentials.
48    Include,
49    /// `same-origin` value of credentials.
50    SameOrigin,
51}
52
53/// Type to set mode for fetch.
54#[derive(Serialize, Debug)]
55#[serde(rename_all = "kebab-case")]
56pub enum Mode {
57    /// `same-origin` value of mode.
58    SameOrigin,
59    /// `no-cors` value of mode.
60    NoCors,
61    /// `cors` value of mode.
62    Cors,
63}
64
65/// Type to set redirect behaviour for fetch.
66#[derive(Serialize, Debug)]
67#[serde(rename_all = "kebab-case")]
68pub enum Redirect {
69    /// `follow` value of redirect.
70    Follow,
71    /// `error` value of redirect.
72    Error,
73    /// `manual` value of redirect.
74    Manual,
75}
76
77impl Serialize for Referrer {
78    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
79    where
80        S: serde::Serializer,
81    {
82        match *self {
83            Referrer::SameOriginUrl(ref s) => serializer.serialize_str(s),
84            Referrer::AboutClient => {
85                serializer.serialize_unit_variant("Referrer", 0, "about:client")
86            }
87            Referrer::Empty => serializer.serialize_unit_variant("Referrer", 1, ""),
88        }
89    }
90}
91
92/// Type to set referrer policy for fetch.
93#[derive(Serialize, Debug)]
94#[serde(rename_all = "kebab-case")]
95pub enum ReferrerPolicy {
96    /// `no-referrer` value of referrerPolicy.
97    NoReferrer,
98    /// `no-referrer-when-downgrade` value of referrerPolicy.
99    NoReferrerWhenDowngrade,
100    /// `same-origin` value of referrerPolicy.
101    SameOrigin,
102    /// `origin` value of referrerPolicy.
103    Origin,
104    /// `strict-origin` value of referrerPolicy.
105    StrictOrigin,
106    /// `origin-when-cross-origin` value of referrerPolicy.
107    OriginWhenCrossOrigin,
108    /// `strict-origin-when-cross-origin` value of referrerPolicy.
109    StrictOriginWhenCrossOrigin,
110    /// `unsafe-url` value of referrerPolicy.
111    UnsafeUrl,
112}
113
114/// Init options for `fetch()` function call.
115/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
116#[derive(Serialize, Default, Debug)]
117#[serde(rename_all = "camelCase")]
118pub struct FetchOptions {
119    /// Cache of a fetch request.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub cache: Option<Cache>,
122    /// Credentials of a fetch request.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub credentials: Option<Credentials>,
125    /// Redirect behaviour of a fetch request.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub redirect: Option<Redirect>,
128    /// Request mode of a fetch request.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub mode: Option<Mode>,
131    /// Referrer of a fetch request.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub referrer: Option<Referrer>,
134    /// Referrer policy of a fetch request.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub referrer_policy: Option<ReferrerPolicy>,
137    /// Integrity of a fetch request.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub integrity: Option<String>,
140}
141
142/// Represents errors of a fetch service.
143#[derive(Debug, Error)]
144enum FetchError {
145    #[error("failed response")]
146    FailedResponse,
147}
148
149/// A handle to control sent requests. Can be canceled with a `Task::cancel` call.
150#[must_use = "the request will be cancelled when the task is dropped"]
151pub struct FetchTask(Option<Value>);
152
153impl fmt::Debug for FetchTask {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        f.write_str("FetchTask")
156    }
157}
158
159/// A service to fetch resources.
160#[derive(Default, Debug)]
161pub struct FetchService {}
162
163impl FetchService {
164    /// Sends a request to a remote server given a Request object and a callback
165    /// function to convert a Response object into a loop's message.
166    ///
167    /// You may use a Request builder to build your request declaratively as on the
168    /// following examples:
169    ///
170    /// ```
171    ///# use yew::format::{Nothing, Json};
172    ///# use yew::services::fetch::Request;
173    ///# use serde_json::json;
174    /// let post_request = Request::post("https://my.api/v1/resource")
175    ///     .header("Content-Type", "application/json")
176    ///     .body(Json(&json!({"foo": "bar"})))
177    ///     .expect("Failed to build request.");
178    ///
179    /// let get_request = Request::get("https://my.api/v1/resource")
180    ///     .body(Nothing)
181    ///     .expect("Failed to build request.");
182    /// ```
183    ///
184    /// The callback function can build a loop message by passing or analyzing the
185    /// response body and metadata.
186    ///
187    /// ```
188    ///# use yew::{Component, ComponentLink, Html};
189    ///# use yew::services::FetchService;
190    ///# use yew::services::fetch::{Response, Request};
191    ///# struct Comp;
192    ///# impl Component for Comp {
193    ///#     type Message = Msg;type Properties = ();
194    ///#     fn create(props: Self::Properties,link: ComponentLink<Self>) -> Self {unimplemented!()}
195    ///#     fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
196    ///#     fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
197    ///#     fn view(&self) -> Html {unimplemented!()}
198    ///# }
199    ///# enum Msg {
200    ///#     Noop,
201    ///#     Error
202    ///# }
203    ///# fn dont_execute() {
204    ///# let link: ComponentLink<Comp> = unimplemented!();
205    ///# let post_request: Request<Result<String, anyhow::Error>> = unimplemented!();
206    /// let task = FetchService::fetch(
207    ///     post_request,
208    ///     link.callback(|response: Response<Result<String, anyhow::Error>>| {
209    ///         if response.status().is_success() {
210    ///             Msg::Noop
211    ///         } else {
212    ///             Msg::Error
213    ///         }
214    ///     }),
215    /// );
216    ///# }
217    /// ```
218    ///
219    /// For a full example, you can specify that the response must be in the JSON format,
220    /// and be a specific serialized data type. If the mesage isn't Json, or isn't the specified
221    /// data type, then you will get a message indicating failure.
222    ///
223    /// ```
224    ///# use yew::format::{Json, Nothing, Format};
225    ///# use yew::services::FetchService;
226    ///# use http::Request;
227    ///# use yew::services::fetch::Response;
228    ///# use yew::{Component, ComponentLink, Html};
229    ///# use serde_derive::Deserialize;
230    ///# struct Comp;
231    ///# impl Component for Comp {
232    ///#     type Message = Msg;type Properties = ();
233    ///#     fn create(props: Self::Properties,link: ComponentLink<Self>) -> Self {unimplemented!()}
234    ///#     fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
235    ///#     fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
236    ///#     fn view(&self) -> Html {unimplemented!()}
237    ///# }
238    ///# enum Msg {
239    ///#     FetchResourceComplete(Data),
240    ///#     FetchResourceFailed
241    ///# }
242    /// #[derive(Deserialize)]
243    /// struct Data {
244    ///    value: String
245    /// }
246    ///
247    ///# fn dont_execute() {
248    ///# let link: ComponentLink<Comp> = unimplemented!();
249    /// let get_request = Request::get("/thing").body(Nothing).unwrap();
250    /// let callback = link.callback(|response: Response<Json<Result<Data, anyhow::Error>>>| {
251    ///     if let (meta, Json(Ok(body))) = response.into_parts() {
252    ///         if meta.status.is_success() {
253    ///             return Msg::FetchResourceComplete(body);
254    ///         }
255    ///     }
256    ///     Msg::FetchResourceFailed
257    /// });
258    ///
259    /// let task = FetchService::fetch(get_request, callback);
260    ///# }
261    /// ```
262    ///
263    pub fn fetch<IN, OUT: 'static>(
264        request: Request<IN>,
265        callback: Callback<Response<OUT>>,
266    ) -> Result<FetchTask, &'static str>
267    where
268        IN: Into<Text>,
269        OUT: From<Text>,
270    {
271        fetch_impl::<IN, OUT, String, String>(false, request, None, callback)
272    }
273
274    /// `fetch` with provided `FetchOptions` object.
275    /// Use it if you need to send cookies with a request:
276    /// ```
277    ///# use yew::format::Nothing;
278    ///# use yew::services::fetch::{self, FetchOptions, Credentials};
279    ///# use yew::{Html, Component, ComponentLink};
280    ///# use yew::services::FetchService;
281    ///# use http::Response;
282    ///# struct Comp;
283    ///# impl Component for Comp {
284    ///#     type Message = Msg;
285    ///#     type Properties = ();
286    ///#     fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {unimplemented!()}
287    ///#     fn update(&mut self, msg: Self::Message) -> bool {unimplemented!()}
288    ///#     fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
289    ///#     fn view(&self) -> Html {unimplemented!()}
290    ///# }
291    ///# pub enum Msg { }
292    ///# fn dont_execute() {
293    ///# let link: ComponentLink<Comp> = unimplemented!();
294    ///# let callback = link.callback(|response: Response<Result<String, anyhow::Error>>| -> Msg { unimplemented!() });
295    /// let request = fetch::Request::get("/path/")
296    ///     .body(Nothing)
297    ///     .unwrap();
298    /// let options = FetchOptions {
299    ///     credentials: Some(Credentials::SameOrigin),
300    ///     ..FetchOptions::default()
301    /// };
302    /// let task = FetchService::fetch_with_options(request, options, callback);
303    ///# }
304    /// ```
305    pub fn fetch_with_options<IN, OUT: 'static>(
306        request: Request<IN>,
307        options: FetchOptions,
308        callback: Callback<Response<OUT>>,
309    ) -> Result<FetchTask, &'static str>
310    where
311        IN: Into<Text>,
312        OUT: From<Text>,
313    {
314        fetch_impl::<IN, OUT, String, String>(false, request, Some(options), callback)
315    }
316
317    /// Fetch the data in binary format.
318    pub fn fetch_binary<IN, OUT: 'static>(
319        request: Request<IN>,
320        callback: Callback<Response<OUT>>,
321    ) -> Result<FetchTask, &'static str>
322    where
323        IN: Into<Binary>,
324        OUT: From<Binary>,
325    {
326        fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, None, callback)
327    }
328
329    /// Fetch the data in binary format using the provided request options.
330    pub fn fetch_binary_with_options<IN, OUT: 'static>(
331        request: Request<IN>,
332        options: FetchOptions,
333        callback: Callback<Response<OUT>>,
334    ) -> Result<FetchTask, &'static str>
335    where
336        IN: Into<Binary>,
337        OUT: From<Binary>,
338    {
339        fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, Some(options), callback)
340    }
341}
342
343fn fetch_impl<IN, OUT: 'static, T, X>(
344    binary: bool,
345    request: Request<IN>,
346    options: Option<FetchOptions>,
347    callback: Callback<Response<OUT>>,
348) -> Result<FetchTask, &'static str>
349where
350    IN: Into<Format<T>>,
351    OUT: From<Format<T>>,
352    T: JsSerialize,
353    X: TryFrom<Value> + Into<T>,
354{
355    // Consume request as parts and body.
356    let (parts, body) = request.into_parts();
357
358    // Map headers into a Js `Header` to make sure it's supported.
359    let header_list = parts
360        .headers
361        .iter()
362        .map(|(k, v)| {
363            Ok((
364                k.as_str(),
365                v.to_str().map_err(|_| "Unparsable request header")?,
366            ))
367        })
368        .collect::<Result<HashMap<_, _>, _>>()?;
369    let header_map = js! {
370        try {
371            return new Headers(@{header_list});
372        } catch(error) {
373            return error;
374        }
375    };
376    if Error::try_from(js!( return @{header_map.as_ref()}; )).is_ok() {
377        return Err("couldn't build headers");
378    }
379
380    // Formats URI.
381    let uri = parts.uri.to_string();
382    let method = parts.method.as_str();
383    let body = body.into().ok();
384
385    // Prepare the response callback.
386    // Notice that the callback signature must match the call from the JavaScript
387    // side. There is no static check at this point.
388    let callback = move |success: bool, status: u16, headers: HashMap<String, String>, data: X| {
389        let mut response_builder = Response::builder();
390
391        if let Ok(status) = StatusCode::from_u16(status) {
392            response_builder = response_builder.status(status);
393        }
394
395        for (key, values) in headers {
396            response_builder = response_builder.header(key.as_str(), values.as_str());
397        }
398
399        // Deserialize and wrap response data into a Text object.
400        let data = if success {
401            Ok(data.into())
402        } else {
403            Err(FetchError::FailedResponse.into())
404        };
405        let out = OUT::from(data);
406        let response = response_builder.body(out).unwrap();
407        callback.emit(response);
408    };
409
410    #[allow(clippy::too_many_arguments)]
411    let handle = js! {
412        var body = @{body};
413        if (@{binary} && body != null) {
414            body = Uint8Array.from(body);
415        }
416        var callback = @{callback};
417        var abortController = AbortController ? new AbortController() : null;
418        var handle = {
419            active: true,
420            callback,
421            abortController,
422        };
423        var init = {
424            method: @{method},
425            body: body,
426            headers: @{header_map},
427        };
428        var opts = @{Serde(options)} || {};
429        for (var attrname in opts) {
430            init[attrname] = opts[attrname];
431        }
432        if (abortController && !("signal" in init)) {
433            init.signal = abortController.signal;
434        }
435        fetch(@{uri}, init).then(function(response) {
436            var promise = (@{binary}) ? response.arrayBuffer() : response.text();
437            var status = response.status;
438            var headers = {};
439            response.headers.forEach(function(value, key) {
440                headers[key] = value;
441            });
442            promise.then(function(data) {
443                if (handle.active == true) {
444                    handle.active = false;
445                    callback(true, status, headers, data);
446                    callback.drop();
447                }
448            }).catch(function(err) {
449                if (handle.active == true) {
450                    handle.active = false;
451                    callback(false, status, headers, data);
452                    callback.drop();
453                }
454            });
455        }).catch(function(e) {
456            if (handle.active == true) {
457                var data = (@{binary}) ? new ArrayBuffer() : "";
458                handle.active = false;
459                callback(false, 408, {}, data);
460                callback.drop();
461            }
462        });
463        return handle;
464    };
465    Ok(FetchTask(Some(handle)))
466}
467
468impl Task for FetchTask {
469    fn is_active(&self) -> bool {
470        if let Some(ref task) = self.0 {
471            let result = js! {
472                var the_task = @{task};
473                return the_task.active &&
474                        (!the_task.abortController || !the_task.abortController.signal.aborted);
475            };
476            result.try_into().unwrap_or(false)
477        } else {
478            false
479        }
480    }
481}
482
483impl Drop for FetchTask {
484    fn drop(&mut self) {
485        if self.is_active() {
486            // Fetch API doesn't support request cancelling in all browsers
487            // and we should use this workaround with a flag.
488            // In that case, request not canceled, but callback won't be called.
489            let handle = self
490                .0
491                .take()
492                .expect("tried to cancel request fetching twice");
493            js! {  @(no_return)
494                var handle = @{handle};
495                handle.active = false;
496                handle.callback.drop();
497                if (handle.abortController) {
498                    handle.abortController.abort();
499                }
500            }
501        }
502    }
503}