Skip to main content

libfw_client/
error.rs

1//! Unified error type surfaced by the WASM engine.
2//!
3//! Every network, storage and protocol failure is converted into a
4//! [`LibfwError`] and finally into a JS `Error` (via
5//! [`LibfwError::to_js`]) so the JS SDK can wrap it in `LibfwError`.
6
7use wasm_bindgen::JsValue;
8
9/// Errors produced by the WASM engine.
10#[derive(Debug, thiserror::Error)]
11pub enum LibfwError {
12    /// The server answered with an unexpected status code.
13    #[error("http {status} for `{url}`")]
14    Http { status: u16, url: String },
15    /// A network-level failure (fetch rejected, body stream broke, …).
16    #[error("network error: {0}")]
17    Network(String),
18    /// The server's body could not be decompressed.
19    #[error("decompression error: {0}")]
20    Decompress(String),
21    /// An upload chunk could not be compressed.
22    #[error("compression error: {0}")]
23    Compress(String),
24    /// The transfer protocol contract was violated.
25    #[error("protocol error: {0}")]
26    Protocol(String),
27    /// A JS callback returned a non-`Promise`/rejected value.
28    #[error("js error: {0}")]
29    Js(String),
30    /// The task was cancelled by the user.
31    #[error("transfer cancelled")]
32    Cancelled,
33    /// An upload file is missing or unreadable.
34    #[error("storage error: {0}")]
35    Storage(String),
36}
37
38impl LibfwError {
39    /// Convert into a JS `Error`-compatible `JsValue`.
40    pub fn to_js(&self) -> JsValue {
41        let msg = self.to_string();
42        let err = js_sys::Error::new(&msg);
43        // Tag it so the JS SDK can distinguish `LibfwError`s.
44        let _ = js_sys::Reflect::set(
45            &err,
46            &JsValue::from_str("isLibfwError"),
47            &JsValue::TRUE,
48        );
49        err.into()
50    }
51}
52
53impl From<JsValue> for LibfwError {
54    fn from(value: JsValue) -> Self {
55        LibfwError::Js(
56            value
57                .as_string()
58                .unwrap_or_else(|| format!("{value:?}")),
59        )
60    }
61}
62
63impl From<libfw_core::error::DecompressError> for LibfwError {
64    fn from(e: libfw_core::error::DecompressError) -> Self {
65        LibfwError::Decompress(e.to_string())
66    }
67}
68
69impl From<libfw_core::error::CompressError> for LibfwError {
70    fn from(e: libfw_core::error::CompressError) -> Self {
71        LibfwError::Compress(e.to_string())
72    }
73}
74
75/// Human-readable form of an arbitrary `JsValue` (for error messages).
76pub fn js_value_string(v: &JsValue) -> String {
77    v.as_string().unwrap_or_else(|| format!("{v:?}"))
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn error_messages_are_stable() {
86        assert_eq!(
87            LibfwError::Http {
88                status: 404,
89                url: "/f".into()
90            }
91            .to_string(),
92            "http 404 for `/f`"
93        );
94        assert_eq!(LibfwError::Cancelled.to_string(), "transfer cancelled");
95    }
96
97    #[test]
98    #[cfg(target_arch = "wasm32")]
99    fn js_errors_carry_tag() {
100        let js = LibfwError::Cancelled.to_js();
101        let err = js_sys::Error::from(js);
102        let tag = js_sys::Reflect::get(&err, &JsValue::from_str("isLibfwError"))
103            .ok()
104            .and_then(|v| v.as_bool())
105            .unwrap_or(false);
106        assert!(tag);
107    }
108}