Thanks to visit codestin.com
Credit goes to docs.rs

Skip to main content

worker/
error.rs

1use crate::kv::KvError;
2use js_sys::Reflect;
3use strum::IntoStaticStr;
4use wasm_bindgen::{JsCast, JsValue};
5
6/// All possible Error variants that might be encountered while working with a Worker.
7#[derive(Debug, IntoStaticStr)]
8#[non_exhaustive]
9pub enum Error {
10    BadEncoding,
11    BodyUsed,
12    Json((String, u16)),
13    /// Error used for strings thrown from JS
14    JsError(String),
15    #[cfg(feature = "http")]
16    Http(http::Error),
17    Infallible,
18    /// Error used for unknown JS values thrown from JS
19    Internal(JsValue),
20    Io(std::io::Error),
21    BindingError(String),
22    RouteInsertError(matchit::InsertError),
23    RouteNoDataError,
24    RustError(String),
25    SerdeJsonError(serde_json::Error),
26    SerdeWasmBindgenError(serde_wasm_bindgen::Error),
27    #[cfg(feature = "http")]
28    StatusCode(http::status::InvalidStatusCode),
29    #[cfg(feature = "d1")]
30    D1(crate::d1::D1Error),
31    Utf8Error(std::str::Utf8Error),
32    #[cfg(feature = "timezone")]
33    TimezoneError,
34    KvError(KvError),
35
36    // Email errors
37    /// Email recipient is on the suppression list (typically due to prior
38    /// bounces or complaints) and cannot be delivered to.
39    EmailRecipientSuppressed(String),
40    /// Email recipient is not allowed for this sender (policy/restriction).
41    ///
42    /// Distinct from `EmailRecipientSuppressed`: suppression is a delivery
43    /// outcome, "not allowed" is a sender-policy refusal.
44    EmailRecipientNotAllowed(String),
45
46    // General errors
47    /// Cloudflare rate limit exceeded.
48    ///
49    /// Currently only produced by the email forwarder. May be produced by other
50    /// rate-limited subsystems in the future.
51    RateLimitExceeded(String),
52    /// Cloudflare daily limit exceeded.
53    ///
54    /// Currently only produced by the email forwarder. May be produced by other
55    /// quota-limited subsystems in the future.
56    DailyLimitExceeded(String),
57    /// A backend Cloudflare service reported an internal error.
58    ///
59    /// Generally transient; consider retrying with backoff.
60    ///
61    /// Currently only produced by the email forwarder. May be produced by other
62    /// subsystems in the future.
63    InternalError(String),
64
65    /// A JavaScript error that didn't match any structured variant.
66    ///
67    /// This is the catch-all for errors thrown by the Workers runtime or
68    /// user code that don't carry a recognized error code.
69    ///
70    /// Always produced by converting from a `JsValue`; not intended to be
71    /// constructed directly from Rust code. Round-tripping back to `JsValue`
72    /// returns the stored `original`, so the original error object identity
73    /// (stack, prototype, extra properties) is preserved.
74    UnknownJsError {
75        /// The original error value.
76        original: JsValue,
77        /// Cached `name` extracted at conversion time.
78        name: Option<String>,
79        /// Cached `message` extracted at conversion time.
80        message: String,
81        /// Cached `code` extracted at conversion time.
82        code: Option<String>,
83        /// Recursively converted `cause`.
84        cause: Option<Box<Error>>,
85    },
86}
87
88const MAX_CAUSE_DEPTH: u32 = 16;
89fn convert_js_error_with_depth(err: js_sys::Error, depth: u32) -> Error {
90    let message: String = err.message().into();
91    let name = err.name().as_string();
92    let code = Reflect::get_str(&err, &"code".into())
93        .ok()
94        .flatten()
95        .and_then(|v| v.as_string());
96    if let Some(code_str) = &code {
97        match code_str.as_str() {
98            "E_RECIPIENT_SUPPRESSED" => return Error::EmailRecipientSuppressed(message),
99            "RCPT_NOT_ALLOWED" => return Error::EmailRecipientNotAllowed(message),
100            "E_RATE_LIMIT_EXCEEDED" => return Error::RateLimitExceeded(message),
101            "E_DAILY_LIMIT_EXCEEDED" => return Error::DailyLimitExceeded(message),
102            "E_INTERNAL_SERVER_ERROR" => return Error::InternalError(message),
103            _ => {} // fall through
104        }
105    }
106    let cause = convert_js_cause(err.cause(), depth);
107    Error::UnknownJsError {
108        original: err.into(),
109        name,
110        message,
111        code,
112        cause,
113    }
114}
115
116/// Classify an arbitrary `JsValue` into an `Error`, threading depth so any
117/// nested `js_sys::Error` causes are decoded with one less hop available.
118fn from_js_value_with_depth(v: JsValue, depth: u32) -> Error {
119    if let Ok(err) = v.clone().dyn_into::<js_sys::Error>() {
120        return convert_js_error_with_depth(err, depth);
121    }
122    if let Some(message) = v.as_string() {
123        return Error::JsError(message);
124    }
125    Error::Internal(v)
126}
127
128fn convert_js_cause(cause: JsValue, depth: u32) -> Option<Box<Error>> {
129    if cause.is_null_or_undefined() || depth == 0 {
130        return None;
131    }
132    Some(Box::new(from_js_value_with_depth(cause, depth - 1)))
133}
134
135impl From<js_sys::Error> for Error {
136    fn from(err: js_sys::Error) -> Self {
137        convert_js_error_with_depth(err, MAX_CAUSE_DEPTH)
138    }
139}
140
141#[cfg(feature = "http")]
142impl From<http::Error> for Error {
143    fn from(value: http::Error) -> Self {
144        Self::Http(value)
145    }
146}
147
148#[cfg(feature = "http")]
149impl From<http::status::InvalidStatusCode> for Error {
150    fn from(value: http::status::InvalidStatusCode) -> Self {
151        Self::StatusCode(value)
152    }
153}
154
155#[cfg(feature = "http")]
156impl From<http::header::InvalidHeaderName> for Error {
157    fn from(value: http::header::InvalidHeaderName) -> Self {
158        Self::RustError(format!("Invalid header name: {value:?}"))
159    }
160}
161
162#[cfg(feature = "http")]
163impl From<http::header::InvalidHeaderValue> for Error {
164    fn from(value: http::header::InvalidHeaderValue) -> Self {
165        Self::RustError(format!("Invalid header value: {value:?}"))
166    }
167}
168
169#[cfg(feature = "timezone")]
170impl From<chrono_tz::ParseError> for Error {
171    fn from(_value: chrono_tz::ParseError) -> Self {
172        Self::RustError("Invalid timezone".to_string())
173    }
174}
175
176impl From<std::str::Utf8Error> for Error {
177    fn from(value: std::str::Utf8Error) -> Self {
178        Self::Utf8Error(value)
179    }
180}
181
182impl From<core::convert::Infallible> for Error {
183    fn from(_value: core::convert::Infallible) -> Self {
184        Error::Infallible
185    }
186}
187
188impl From<KvError> for Error {
189    fn from(e: KvError) -> Self {
190        Self::KvError(e)
191    }
192}
193
194impl From<url::ParseError> for Error {
195    fn from(e: url::ParseError) -> Self {
196        Self::RustError(e.to_string())
197    }
198}
199
200impl From<serde_urlencoded::de::Error> for Error {
201    fn from(e: serde_urlencoded::de::Error) -> Self {
202        Self::RustError(e.to_string())
203    }
204}
205
206impl From<serde_wasm_bindgen::Error> for Error {
207    fn from(e: serde_wasm_bindgen::Error) -> Self {
208        let val: JsValue = e.into();
209        val.into()
210    }
211}
212
213#[cfg(feature = "d1")]
214impl From<crate::d1::D1Error> for Error {
215    fn from(e: crate::d1::D1Error) -> Self {
216        Self::D1(e)
217    }
218}
219
220impl std::fmt::Display for Error {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        match self {
223            // Leaf errors: no inner data
224            Error::BadEncoding => f.write_str("content-type mismatch"),
225            Error::BodyUsed => f.write_str("body has already been read"),
226            Error::Infallible => f.write_str("infallible"),
227            Error::RouteNoDataError => f.write_str("route has no corresponding shared data"),
228            #[cfg(feature = "timezone")]
229            Error::TimezoneError => f.write_str("invalid timezone"),
230            // String-carrying variants: no source, inline the data
231            Error::JsError(s) => f.write_str(s),
232            Error::RustError(s) => f.write_str(s),
233            Error::BindingError(name) => write!(f, "no binding found for `{name}`"),
234            Error::Json((msg, status)) => write!(f, "{msg} (status: {status})"),
235            // Wrapped Rust errors: source() exposes the inner; here we just
236            // categorize. Avoids duplication when consumers walk the chain.
237            Error::Io(_) => f.write_str("I/O error"),
238            Error::Utf8Error(_) => f.write_str("UTF-8 decoding error"),
239            Error::SerdeJsonError(_) => f.write_str("JSON serialization error"),
240            Error::SerdeWasmBindgenError(_) => f.write_str("wasm-bindgen serialization error"),
241            Error::RouteInsertError(_) => f.write_str("failed to insert route"),
242            #[cfg(feature = "http")]
243            Error::Http(_) => f.write_str("HTTP error"),
244            #[cfg(feature = "http")]
245            Error::StatusCode(_) => f.write_str("invalid HTTP status code"),
246            // Wrapped types we're not refactoring right now
247            #[cfg(feature = "d1")]
248            Error::D1(e) => write!(f, "D1: {e:#?}"),
249            Error::KvError(KvError::JavaScript(s)) => write!(f, "js error: {s:?}"),
250            Error::KvError(KvError::Serialization(s)) => {
251                write!(f, "unable to serialize/deserialize: {s}")
252            }
253            Error::KvError(KvError::InvalidKvStore(s)) => write!(f, "invalid kv store: {s}"),
254            // Opaque JS value
255            Error::Internal(v) => write!(f, "unrecognized JavaScript value: {v:?}"),
256            // Email/coded variants: `VariantName: <upstream message>`.
257            // PascalCase variant name acts as a stable, greppable identifier;
258            // the upstream message provides specific context.
259            Error::EmailRecipientSuppressed(msg)
260            | Error::EmailRecipientNotAllowed(msg)
261            | Error::RateLimitExceeded(msg)
262            | Error::DailyLimitExceeded(msg)
263            | Error::InternalError(msg) => {
264                let name: &'static str = self.into();
265                write!(f, "{name}: {msg}")
266            }
267            // JS catch-all: Node-style `Name [code]: message`
268            Error::UnknownJsError {
269                name,
270                message,
271                code,
272                ..
273            } => {
274                let prefix = name.as_deref().unwrap_or("Error");
275                match code {
276                    Some(c) => write!(f, "{prefix} [{c}]: {message}"),
277                    None => write!(f, "{prefix}: {message}"),
278                }
279            }
280        }
281    }
282}
283
284impl std::error::Error for Error {
285    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
286        match self {
287            // Wrapped Rust errors
288            Error::Io(e) => Some(e),
289            Error::Utf8Error(e) => Some(e),
290            Error::SerdeJsonError(e) => Some(e),
291            Error::SerdeWasmBindgenError(e) => Some(e),
292            Error::RouteInsertError(e) => Some(e),
293            #[cfg(feature = "http")]
294            Error::Http(e) => Some(e),
295            #[cfg(feature = "http")]
296            Error::StatusCode(e) => Some(e),
297            // JS error chain
298            Error::UnknownJsError { cause: Some(c), .. } => Some(c.as_ref()),
299            // Everything else: leaf errors
300            _ => None,
301        }
302    }
303}
304
305impl From<JsValue> for Error {
306    fn from(v: JsValue) -> Self {
307        // Top-level conversion: full depth budget for any nested cause chain.
308        // Real Error objects flow through `convert_js_error_with_depth` for
309        // coded-error decoding (email codes, cause extraction).
310        // Bare strings become `JsError`. Other JS values become `Internal`.
311        from_js_value_with_depth(v, MAX_CAUSE_DEPTH)
312    }
313}
314
315impl From<std::io::Error> for Error {
316    fn from(error: std::io::Error) -> Self {
317        Self::Io(error)
318    }
319}
320
321impl Error {
322    /// Build a `JsValue` representation of this error without consuming it.
323    ///
324    /// For variants holding a stored `JsValue` (`UnknownJsError`, `Internal`),
325    /// this clones the original — cheap, since `JsValue::clone` is a refcount
326    /// bump on the V8 anchor table. Other variants reconstruct a fresh
327    /// `js_sys::Error` with the same code/message as the consuming
328    /// `From<Error> for JsValue` impl.
329    pub fn to_js_value(&self) -> JsValue {
330        match self {
331            // Stored originals: clone the underlying handle.
332            Error::UnknownJsError { original, .. } => original.clone(),
333            Error::Internal(v) => v.clone(),
334            // Bare string — matches what JS originally threw.
335            Error::JsError(s) => JsValue::from_str(s),
336            // Coded variants: reconstruct a js_sys::Error with the canonical code.
337            // Preserves the contract that JS callers can branch on err.code.
338            Error::EmailRecipientSuppressed(msg) => {
339                build_coded_error(msg, "E_RECIPIENT_SUPPRESSED")
340            }
341            Error::EmailRecipientNotAllowed(msg) => build_coded_error(msg, "RCPT_NOT_ALLOWED"),
342            Error::RateLimitExceeded(msg) => build_coded_error(msg, "E_RATE_LIMIT_EXCEEDED"),
343            Error::DailyLimitExceeded(msg) => build_coded_error(msg, "E_DAILY_LIMIT_EXCEEDED"),
344            Error::InternalError(msg) => build_coded_error(msg, "E_INTERNAL_SERVER_ERROR"),
345            // Everything else: build a plain js_sys::Error from the
346            // chain-walked Display string, so source() info isn't lost.
347            other => js_sys::Error::new(&format_with_chain(other)).into(),
348        }
349    }
350}
351
352impl From<Error> for JsValue {
353    fn from(e: Error) -> Self {
354        match e {
355            // Move out of variants holding a JsValue — avoids the clone that
356            // `to_js_value` would do. Functionally identical otherwise.
357            Error::UnknownJsError { original, .. } => original,
358            Error::Internal(v) => v,
359            other => other.to_js_value(),
360        }
361    }
362}
363
364/// Walk an error's `source()` chain, formatting each cause on its own
365/// indented line under a `Caused by:` header. Matches the convention used
366/// by `anyhow` and `eyre`. Used when serializing back to a `JsValue` so
367/// the full chain is preserved in the rendered string.
368fn format_with_chain(e: &(dyn std::error::Error)) -> String {
369    let mut s = e.to_string();
370    let mut current = e.source();
371    while let Some(src) = current {
372        s.push_str("\nCaused by:\n    ");
373        s.push_str(&src.to_string());
374        current = src.source();
375    }
376    s
377}
378
379fn build_coded_error(message: &str, code: &str) -> JsValue {
380    let err = js_sys::Error::new(message);
381    let _ = Reflect::set(&err, &"code".into(), &JsValue::from_str(code));
382    err.into()
383}
384
385impl From<&str> for Error {
386    fn from(a: &str) -> Self {
387        Error::RustError(a.to_string())
388    }
389}
390
391impl From<String> for Error {
392    fn from(a: String) -> Self {
393        Error::RustError(a)
394    }
395}
396
397impl From<matchit::InsertError> for Error {
398    fn from(e: matchit::InsertError) -> Self {
399        Error::RouteInsertError(e)
400    }
401}
402
403impl From<serde_json::Error> for Error {
404    fn from(e: serde_json::Error) -> Self {
405        Error::SerdeJsonError(e)
406    }
407}