Skip to main content

fusor_async/
fetch.rs

1//! An abortable GET adapter for loaders. HTTP caching follows Fetch defaults.
2//! For other methods, headers or bodies, build the request yourself and pass
3//! [`CancellationToken::abort_signal`] as its signal.
4use crate::CancellationToken;
5use std::fmt;
6use wasm_bindgen::{JsCast, JsValue};
7use wasm_bindgen_futures::JsFuture;
8use web_sys::{RequestInit, Response};
9
10#[wasm_bindgen::prelude::wasm_bindgen(
11    inline_js = "export async function fusorFetch(url, options) { const base = globalThis.__fusor_worker_base_url; return globalThis.fetch(base ? new URL(url, base).href : url, options); }"
12)]
13extern "C" {
14    #[wasm_bindgen::prelude::wasm_bindgen(js_name = fusorFetch)]
15    fn fetch(url: &str, options: &RequestInit) -> js_sys::Promise;
16}
17
18/// Why [`get_text`] failed.
19#[derive(Debug)]
20pub enum FetchError {
21    /// `cancel` fired before the response body was fully read.
22    Cancelled,
23    /// The server answered with a non-2xx status.
24    Status { url: String, status: u16 },
25    /// Fetch or body reading failed in the browser.
26    Js(JsValue),
27}
28impl From<JsValue> for FetchError {
29    fn from(error: JsValue) -> Self {
30        Self::Js(error)
31    }
32}
33impl fmt::Display for FetchError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Cancelled => f.write_str("request cancelled"),
37            Self::Status { url, status } => write!(f, "GET {url}: HTTP {status}"),
38            Self::Js(error) => match error.as_string() {
39                Some(message) => f.write_str(&message),
40                None => write!(f, "{error:?}"),
41            },
42        }
43    }
44}
45impl std::error::Error for FetchError {}
46
47/// GET and fully read a UTF-8 response body. Rejects non-2xx responses.
48/// `cancel` aborts the Fetch at any point, including while the body is read.
49pub async fn get_text(url: &str, cancel: &CancellationToken) -> Result<String, FetchError> {
50    let result: Result<String, FetchError> = async {
51        let options = RequestInit::new();
52        options.set_signal(Some(&cancel.abort_signal()?));
53        let response: Response = JsFuture::from(fetch(url, &options)).await?.dyn_into()?;
54        if !response.ok() {
55            return Err(FetchError::Status {
56                url: url.to_owned(),
57                status: response.status(),
58            });
59        }
60        JsFuture::from(response.text()?)
61            .await?
62            .as_string()
63            .ok_or_else(|| JsValue::from_str("Fetch returned a non-text body").into())
64    }
65    .await;
66    // An aborted Fetch rejects with a DOM `AbortError`; report it as cancellation.
67    result.map_err(|error| match error {
68        FetchError::Js(_) if cancel.is_cancelled() => FetchError::Cancelled,
69        error => error,
70    })
71}