use crate::kv::KvError;
use js_sys::Reflect;
use strum::IntoStaticStr;
use wasm_bindgen::{JsCast, JsValue};
#[derive(Debug, IntoStaticStr)]
#[non_exhaustive]
pub enum Error {
BadEncoding,
BodyUsed,
Json((String, u16)),
JsError(String),
#[cfg(feature = "http")]
Http(http::Error),
Infallible,
Internal(JsValue),
Io(std::io::Error),
BindingError(String),
RouteInsertError(matchit::InsertError),
RouteNoDataError,
RustError(String),
SerdeJsonError(serde_json::Error),
SerdeWasmBindgenError(serde_wasm_bindgen::Error),
#[cfg(feature = "http")]
StatusCode(http::status::InvalidStatusCode),
#[cfg(feature = "d1")]
D1(crate::d1::D1Error),
Utf8Error(std::str::Utf8Error),
#[cfg(feature = "timezone")]
TimezoneError,
KvError(KvError),
EmailRecipientSuppressed(String),
EmailRecipientNotAllowed(String),
RateLimitExceeded(String),
DailyLimitExceeded(String),
InternalError(String),
UnknownJsError {
original: JsValue,
name: Option<String>,
message: String,
code: Option<String>,
cause: Option<Box<Error>>,
},
}
const MAX_CAUSE_DEPTH: u32 = 16;
fn convert_js_error_with_depth(err: js_sys::Error, depth: u32) -> Error {
let message: String = err.message().into();
let name = err.name().as_string();
let code = Reflect::get_str(&err, &"code".into())
.ok()
.flatten()
.and_then(|v| v.as_string());
if let Some(code_str) = &code {
match code_str.as_str() {
"E_RECIPIENT_SUPPRESSED" => return Error::EmailRecipientSuppressed(message),
"RCPT_NOT_ALLOWED" => return Error::EmailRecipientNotAllowed(message),
"E_RATE_LIMIT_EXCEEDED" => return Error::RateLimitExceeded(message),
"E_DAILY_LIMIT_EXCEEDED" => return Error::DailyLimitExceeded(message),
"E_INTERNAL_SERVER_ERROR" => return Error::InternalError(message),
_ => {} }
}
let cause = convert_js_cause(err.cause(), depth);
Error::UnknownJsError {
original: err.into(),
name,
message,
code,
cause,
}
}
fn from_js_value_with_depth(v: JsValue, depth: u32) -> Error {
if let Ok(err) = v.clone().dyn_into::<js_sys::Error>() {
return convert_js_error_with_depth(err, depth);
}
if let Some(message) = v.as_string() {
return Error::JsError(message);
}
Error::Internal(v)
}
fn convert_js_cause(cause: JsValue, depth: u32) -> Option<Box<Error>> {
if cause.is_null_or_undefined() || depth == 0 {
return None;
}
Some(Box::new(from_js_value_with_depth(cause, depth - 1)))
}
impl From<js_sys::Error> for Error {
fn from(err: js_sys::Error) -> Self {
convert_js_error_with_depth(err, MAX_CAUSE_DEPTH)
}
}
#[cfg(feature = "http")]
impl From<http::Error> for Error {
fn from(value: http::Error) -> Self {
Self::Http(value)
}
}
#[cfg(feature = "http")]
impl From<http::status::InvalidStatusCode> for Error {
fn from(value: http::status::InvalidStatusCode) -> Self {
Self::StatusCode(value)
}
}
#[cfg(feature = "http")]
impl From<http::header::InvalidHeaderName> for Error {
fn from(value: http::header::InvalidHeaderName) -> Self {
Self::RustError(format!("Invalid header name: {value:?}"))
}
}
#[cfg(feature = "http")]
impl From<http::header::InvalidHeaderValue> for Error {
fn from(value: http::header::InvalidHeaderValue) -> Self {
Self::RustError(format!("Invalid header value: {value:?}"))
}
}
#[cfg(feature = "timezone")]
impl From<chrono_tz::ParseError> for Error {
fn from(_value: chrono_tz::ParseError) -> Self {
Self::RustError("Invalid timezone".to_string())
}
}
impl From<std::str::Utf8Error> for Error {
fn from(value: std::str::Utf8Error) -> Self {
Self::Utf8Error(value)
}
}
impl From<core::convert::Infallible> for Error {
fn from(_value: core::convert::Infallible) -> Self {
Error::Infallible
}
}
impl From<KvError> for Error {
fn from(e: KvError) -> Self {
Self::KvError(e)
}
}
impl From<url::ParseError> for Error {
fn from(e: url::ParseError) -> Self {
Self::RustError(e.to_string())
}
}
impl From<serde_urlencoded::de::Error> for Error {
fn from(e: serde_urlencoded::de::Error) -> Self {
Self::RustError(e.to_string())
}
}
impl From<serde_wasm_bindgen::Error> for Error {
fn from(e: serde_wasm_bindgen::Error) -> Self {
let val: JsValue = e.into();
val.into()
}
}
#[cfg(feature = "d1")]
impl From<crate::d1::D1Error> for Error {
fn from(e: crate::d1::D1Error) -> Self {
Self::D1(e)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::BadEncoding => f.write_str("content-type mismatch"),
Error::BodyUsed => f.write_str("body has already been read"),
Error::Infallible => f.write_str("infallible"),
Error::RouteNoDataError => f.write_str("route has no corresponding shared data"),
#[cfg(feature = "timezone")]
Error::TimezoneError => f.write_str("invalid timezone"),
Error::JsError(s) => f.write_str(s),
Error::RustError(s) => f.write_str(s),
Error::BindingError(name) => write!(f, "no binding found for `{name}`"),
Error::Json((msg, status)) => write!(f, "{msg} (status: {status})"),
Error::Io(_) => f.write_str("I/O error"),
Error::Utf8Error(_) => f.write_str("UTF-8 decoding error"),
Error::SerdeJsonError(_) => f.write_str("JSON serialization error"),
Error::SerdeWasmBindgenError(_) => f.write_str("wasm-bindgen serialization error"),
Error::RouteInsertError(_) => f.write_str("failed to insert route"),
#[cfg(feature = "http")]
Error::Http(_) => f.write_str("HTTP error"),
#[cfg(feature = "http")]
Error::StatusCode(_) => f.write_str("invalid HTTP status code"),
#[cfg(feature = "d1")]
Error::D1(e) => write!(f, "D1: {e:#?}"),
Error::KvError(KvError::JavaScript(s)) => write!(f, "js error: {s:?}"),
Error::KvError(KvError::Serialization(s)) => {
write!(f, "unable to serialize/deserialize: {s}")
}
Error::KvError(KvError::InvalidKvStore(s)) => write!(f, "invalid kv store: {s}"),
Error::Internal(v) => write!(f, "unrecognized JavaScript value: {v:?}"),
Error::EmailRecipientSuppressed(msg)
| Error::EmailRecipientNotAllowed(msg)
| Error::RateLimitExceeded(msg)
| Error::DailyLimitExceeded(msg)
| Error::InternalError(msg) => {
let name: &'static str = self.into();
write!(f, "{name}: {msg}")
}
Error::UnknownJsError {
name,
message,
code,
..
} => {
let prefix = name.as_deref().unwrap_or("Error");
match code {
Some(c) => write!(f, "{prefix} [{c}]: {message}"),
None => write!(f, "{prefix}: {message}"),
}
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
Error::Utf8Error(e) => Some(e),
Error::SerdeJsonError(e) => Some(e),
Error::SerdeWasmBindgenError(e) => Some(e),
Error::RouteInsertError(e) => Some(e),
#[cfg(feature = "http")]
Error::Http(e) => Some(e),
#[cfg(feature = "http")]
Error::StatusCode(e) => Some(e),
Error::UnknownJsError { cause: Some(c), .. } => Some(c.as_ref()),
_ => None,
}
}
}
impl From<JsValue> for Error {
fn from(v: JsValue) -> Self {
from_js_value_with_depth(v, MAX_CAUSE_DEPTH)
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
impl Error {
pub fn to_js_value(&self) -> JsValue {
match self {
Error::UnknownJsError { original, .. } => original.clone(),
Error::Internal(v) => v.clone(),
Error::JsError(s) => JsValue::from_str(s),
Error::EmailRecipientSuppressed(msg) => {
build_coded_error(msg, "E_RECIPIENT_SUPPRESSED")
}
Error::EmailRecipientNotAllowed(msg) => build_coded_error(msg, "RCPT_NOT_ALLOWED"),
Error::RateLimitExceeded(msg) => build_coded_error(msg, "E_RATE_LIMIT_EXCEEDED"),
Error::DailyLimitExceeded(msg) => build_coded_error(msg, "E_DAILY_LIMIT_EXCEEDED"),
Error::InternalError(msg) => build_coded_error(msg, "E_INTERNAL_SERVER_ERROR"),
other => js_sys::Error::new(&format_with_chain(other)).into(),
}
}
}
impl From<Error> for JsValue {
fn from(e: Error) -> Self {
match e {
Error::UnknownJsError { original, .. } => original,
Error::Internal(v) => v,
other => other.to_js_value(),
}
}
}
fn format_with_chain(e: &(dyn std::error::Error)) -> String {
let mut s = e.to_string();
let mut current = e.source();
while let Some(src) = current {
s.push_str("\nCaused by:\n ");
s.push_str(&src.to_string());
current = src.source();
}
s
}
fn build_coded_error(message: &str, code: &str) -> JsValue {
let err = js_sys::Error::new(message);
let _ = Reflect::set(&err, &"code".into(), &JsValue::from_str(code));
err.into()
}
impl From<&str> for Error {
fn from(a: &str) -> Self {
Error::RustError(a.to_string())
}
}
impl From<String> for Error {
fn from(a: String) -> Self {
Error::RustError(a)
}
}
impl From<matchit::InsertError> for Error {
fn from(e: matchit::InsertError) -> Self {
Error::RouteInsertError(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::SerdeJsonError(e)
}
}