diff --git a/examples/01-app-demos/hotdog/src/frontend.rs b/examples/01-app-demos/hotdog/src/frontend.rs index 0e3e0d2cb..1117fa30c 100644 --- a/examples/01-app-demos/hotdog/src/frontend.rs +++ b/examples/01-app-demos/hotdog/src/frontend.rs @@ -42,7 +42,7 @@ pub fn NavBar() -> Element { #[component] pub fn DogView() -> Element { let mut img_src = use_loader(|| async move { - anyhow::Ok( + dioxus::Ok( reqwest::get("https://dog.ceo/api/breeds/image/random") .await? .json::() diff --git a/examples/07-fullstack/server_sent_events.rs b/examples/07-fullstack/server_sent_events.rs index 103c6c37b..a517a80fa 100644 --- a/examples/07-fullstack/server_sent_events.rs +++ b/examples/07-fullstack/server_sent_events.rs @@ -41,7 +41,7 @@ fn app() -> Element { events.push(event); } - anyhow::Ok(()) + dioxus::Ok(()) }); rsx! { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 862d48854..896dbff0b 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -77,8 +77,11 @@ pub(crate) mod innerlude { pub use anyhow::anyhow; pub use anyhow::Context as AnyhowContext; - pub use anyhow::Error; - pub use anyhow::Result; + // pub use anyhow::Error as AnyhowError; + // pub type Error = CapturedError; + + /// A result type with a default error of [`CapturedError`]. + pub type Result = std::result::Result; /// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`ScopeId`] or [`ScopeState`]. /// @@ -97,7 +100,7 @@ pub use crate::innerlude::{ suspend, throw_error, try_consume_context, use_after_render, use_before_render, use_drop, use_hook, use_hook_with_cleanup, with_owner, AnyValue, AnyhowContext, Attribute, AttributeValue, Callback, CapturedError, Component, ComponentFunction, DynamicNode, Element, - ElementId, Error, ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes, + ElementId, ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LaunchConfig, ListenerCallback, MarkerWrapper, Mutation, Mutations, NoOpMutations, OptionStringFromMarker, Properties, ReactiveContext, RenderError, Result, Runtime, RuntimeGuard, ScopeId, ScopeState, SpawnIfAsync, SubscriberList, Subscribers, @@ -106,5 +109,27 @@ pub use crate::innerlude::{ VNodeInner, VPlaceholder, VText, VirtualDom, WriteMutations, }; -pub use anyhow::Ok; +/// Equivalent to `Ok::<_, dioxus::CapturedError>(value)`. +/// +/// This simplifies creation of an `dioxus::Result` in places where type +/// inference cannot deduce the `E` type of the result — without needing +/// to write`Ok::<_, dioxus::CapturedError>(value)`. +/// +/// One might think that `dioxus::Result::Ok(value)` would work in such cases +/// but it does not. +/// +/// ```console +/// error[E0282]: type annotations needed for `std::result::Result` +/// --> src/main.rs:11:13 +/// | +/// 11 | let _ = dioxus::Result::Ok(1); +/// | - ^^^^^^^^^^^^^^^^^^ cannot infer type for type parameter `E` declared on the enum `Result` +/// | | +/// | consider giving this pattern the explicit type `std::result::Result`, where the type parameter `E` is specified +/// ``` +#[allow(non_snake_case)] +pub fn Ok(value: T) -> Result { + Result::Ok(value) +} + pub use const_format; diff --git a/packages/core/src/render_error.rs b/packages/core/src/render_error.rs index 12b38b507..59e803cf4 100644 --- a/packages/core/src/render_error.rs +++ b/packages/core/src/render_error.rs @@ -51,7 +51,7 @@ impl From for RenderError { } } -impl> From for RenderError { +impl> From for RenderError { fn from(e: E) -> Self { let anyhow_err = e.into(); @@ -69,13 +69,44 @@ impl> From for RenderError { /// An `anyhow::Error` wrapped in an `Arc` so it can be cheaply cloned and passed around. #[derive(Debug, Clone)] -pub struct CapturedError(pub Arc); +pub struct CapturedError(pub Arc); impl CapturedError { /// Create a `CapturedError` from anything that implements `Display`. pub fn from_display(t: impl Display) -> Self { Self(Arc::new(anyhow::anyhow!(t.to_string()))) } + + /// Create a `CapturedError` from anything that implements `std::error::Error`. + pub fn new(error: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + anyhow::Error::new(error).into() + } + + /// Create a `CapturedError` from anything that implements `Display` and `Debug`. + pub fn msg(t: M) -> Self + where + M: Display + Debug + Send + Sync + 'static, + { + anyhow::Error::msg(t).into() + } + + /// Create a `CapturedError` from a boxed `std::error::Error`. + pub fn from_boxed(boxed_error: Box) -> Self { + anyhow::Error::from_boxed(boxed_error).into() + } + + /// Returns the strong count of the underlying error. + pub fn _strong_count(&self) -> usize { + std::sync::Arc::strong_count(&self.0) + } + + /// Try to unwrap the underlying error if this is the only reference to it. + pub fn into_inner(self) -> Option { + Arc::try_unwrap(self.0).ok() + } } #[cfg(feature = "serialize")] @@ -100,14 +131,14 @@ impl<'de> serde::Deserialize<'de> for CapturedError { } impl std::ops::Deref for CapturedError { - type Target = Error; + type Target = anyhow::Error; fn deref(&self) -> &Self::Target { &self.0 } } -impl> From for CapturedError { +impl> From for CapturedError { fn from(e: E) -> Self { Self(Arc::new(e.into())) } diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 3c106de44..c74e13811 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -331,7 +331,10 @@ impl NativeFileData for DesktopFileData { fn read_bytes( &self, ) -> std::pin::Pin< - Box> + 'static>, + Box< + dyn std::future::Future> + + 'static, + >, > { let path = self.0.clone(); Box::pin(async move { Ok(bytes::Bytes::from(std::fs::read(&path)?)) }) @@ -340,7 +343,7 @@ impl NativeFileData for DesktopFileData { fn read_string( &self, ) -> std::pin::Pin< - Box> + 'static>, + Box> + 'static>, > { let path = self.0.clone(); Box::pin(async move { Ok(std::fs::read_to_string(&path)?) }) @@ -358,7 +361,7 @@ impl NativeFileData for DesktopFileData { &self, ) -> std::pin::Pin< Box< - dyn futures_util::Stream> + dyn futures_util::Stream> + 'static + Send, >, @@ -374,7 +377,7 @@ impl NativeFileData for DesktopFileData { // let n = file // .read(&mut buf) // .await - // .map_err(|e| dioxus_core::Error::from(e))?; + // .map_err(|e| dioxus_core::CapturedError::from(e))?; // if n == 0 { // Ok(None) // } else { diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 94ad246e9..7f0957623 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -27,7 +27,7 @@ pub use dioxus_core; #[doc(inline)] -pub use dioxus_core::{CapturedError, Error, Ok, Result}; +pub use dioxus_core::{CapturedError, Ok, Result}; #[cfg(feature = "launch")] #[cfg_attr(docsrs, doc(cfg(feature = "launch")))] diff --git a/packages/fullstack-core/src/loader.rs b/packages/fullstack-core/src/loader.rs index dccb80878..8a4a6b23a 100644 --- a/packages/fullstack-core/src/loader.rs +++ b/packages/fullstack-core/src/loader.rs @@ -33,7 +33,7 @@ pub fn use_loader(mut future: impl FnMut() -> F + 'static) -> Result> + 'static, T: 'static + PartialEq + Serialize + DeserializeOwned, - E: Into + 'static, + E: Into + 'static, { let serialize_context = use_hook(crate::transport::serialize_context); @@ -101,8 +101,8 @@ where // Remap the error to the captured error type so it's cheap to clone and pass out, just // slightly more cumbersome to access the inner error. let out = out.map_err(|e| { - let anyhow_err: anyhow::Error = e.into(); - anyhow_err.into() + let anyhow_err: CapturedError = e.into(); + anyhow_err }); // If this is the first run and we are on the server, cache the data in the slot we reserved for it diff --git a/packages/fullstack-core/src/transport.rs b/packages/fullstack-core/src/transport.rs index dc01be8a9..ecc94c267 100644 --- a/packages/fullstack-core/src/transport.rs +++ b/packages/fullstack-core/src/transport.rs @@ -4,7 +4,7 @@ use base64::Engine; use dioxus_core::CapturedError; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{cell::RefCell, io::Cursor, rc::Rc, sync::Arc}; +use std::{cell::RefCell, io::Cursor, rc::Rc}; #[cfg(feature = "web")] thread_local! { @@ -429,15 +429,15 @@ pub fn head_element_hydration_entry() -> SerializeContextEntry { /// A `Transportable` type can be safely transported from the server to the client, and be used for /// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible -/// to customize how types are transported which helps for non-serializable types like `dioxus_core::Error`. +/// to customize how types are transported which helps for non-serializable types like `dioxus_core::CapturedError`. /// /// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable. /// -/// You can also implement `Transportable` for `Result` where `T` is +/// You can also implement `Transportable` for `Result` where `T` is /// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors. /// -/// Note that transporting a `Result` will lose various aspects of the original -/// `dioxus_core::Error` such as backtraces and source errors, but will preserve the error message. +/// Note that transporting a `Result` will lose various aspects of the original +/// `dioxus_core::CapturedError` such as backtraces and source errors, but will preserve the error message. pub trait Transportable: 'static { /// Serialize the type to a byte vector for transport fn transport_to_bytes(&self) -> Vec; @@ -474,7 +474,7 @@ struct TransportResultErr { #[doc(hidden)] pub struct TransportViaErrMarker; -impl Transportable for Result +impl Transportable for Result where T: Serialize + DeserializeOwned + 'static, { @@ -497,7 +497,7 @@ where let err: TransportResultErr = ciborium::from_reader(Cursor::new(bytes))?; match err.error { Ok(value) => Ok(Ok(value)), - Err(captured) => Ok(Err(dioxus_core::Error::msg(captured.to_string()))), + Err(captured) => Ok(Err(anyhow::Error::msg(captured.to_string()))), } } } @@ -525,8 +525,6 @@ impl Transportable for CapturedError { Self: Sized, { let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?; - Ok(CapturedError(Arc::new(dioxus_core::Error::msg::( - err.error, - )))) + Ok(dioxus_core::CapturedError::msg::(err.error)) } } diff --git a/packages/fullstack/src/lazy.rs b/packages/fullstack/src/lazy.rs index 225c57b66..9ef68d770 100644 --- a/packages/fullstack/src/lazy.rs +++ b/packages/fullstack/src/lazy.rs @@ -1,5 +1,6 @@ #![allow(clippy::needless_return)] +use dioxus_core::CapturedError; use std::{hint::black_box, prelude::rust_2024::Future, sync::atomic::AtomicBool}; /// `Lazy` is a thread-safe, lazily-initialized global variable. @@ -14,7 +15,7 @@ use std::{hint::black_box, prelude::rust_2024::Future, sync::atomic::AtomicBool} pub struct Lazy { value: std::sync::OnceLock, started_initialization: AtomicBool, - constructor: Option Result>, + constructor: Option Result>, _phantom: std::marker::PhantomData, } @@ -36,7 +37,7 @@ impl Lazy { where F: Fn() -> G + Copy, G: Future> + Send + 'static, - E: Into, + E: Into, { if std::mem::size_of::() != 0 { panic!("The constructor function must be a zero-sized type (ZST). Consider using a function pointer or a closure without captured variables."); @@ -57,10 +58,10 @@ impl Lazy { /// /// This should only be called once during the server setup phase, typically inside `dioxus::serve`. /// Future calls to this method will return an error containing the provided value. - pub fn set(&self, pool: T) -> Result<(), dioxus_core::Error> { + pub fn set(&self, pool: T) -> Result<(), CapturedError> { let res = self.value.set(pool); if res.is_err() { - return Err(anyhow::anyhow!("Lazy value is already initialized.")); + return Err(anyhow::anyhow!("Lazy value is already initialized.").into()); } Ok(()) @@ -71,7 +72,7 @@ impl Lazy { } /// Initialize the value of the `Lazy` instance if it hasn't been initialized yet. - pub fn initialize(&self) -> Result<(), dioxus_core::Error> { + pub fn initialize(&self) -> Result<(), CapturedError> { if let Some(constructor) = self.constructor { // If we're already initializing this value, wait on the receiver. if self @@ -115,12 +116,12 @@ impl std::ops::Deref for Lazy { /// /// We call the `rust-call` method of the zero-sized constructor function. This is safe because we're /// not actually dereferencing any unsafe data, just calling its vtable entry to get the future. -fn blocking_initialize() -> Result +fn blocking_initialize() -> Result where T: Send + Sync + 'static, F: Fn() -> G + Copy, G: Future> + Send + 'static, - E: Into, + E: Into, { assert_eq!(std::mem::size_of::(), 0, "The constructor function must be a zero-sized type (ZST). Consider using a function pointer or a closure without captured variables."); diff --git a/packages/fullstack/src/magic.rs b/packages/fullstack/src/magic.rs index 9ac3e83ec..2b8851fe5 100644 --- a/packages/fullstack/src/magic.rs +++ b/packages/fullstack/src/magic.rs @@ -566,6 +566,7 @@ mod resp { use super::*; use axum::response::Response; + use dioxus_core::CapturedError; use http::HeaderValue; /// A trait for converting the result of the Server Function into an Axum response. @@ -661,6 +662,71 @@ mod resp { } } + impl MakeAxumError for &&ServerFnDecoder> { + fn make_axum_error( + self, + result: Result, + ) -> Result { + match result { + Ok(res) => Ok(res), + + // Optimize the case where we have sole ownership of the error + Err(errr) if errr._strong_count() == 1 => { + let err = errr.into_inner().unwrap(); + <&&ServerFnDecoder> as MakeAxumError>::make_axum_error( + &&ServerFnDecoder::new(), + Err(err), + ) + } + + Err(errr) => { + // The `WithHttpError` trait emits ServerFnErrors so we can downcast them here + // to create richer responses. + let payload = match errr.downcast_ref::() { + Some(ServerFnError::ServerError { + message, + code, + details, + }) => ErrorPayload { + message: message.clone(), + code: *code, + data: details.clone(), + }, + Some(other) => ErrorPayload { + message: other.to_string(), + code: 500, + data: None, + }, + None => match errr.downcast_ref::() { + Some(http_err) => ErrorPayload { + message: http_err + .message + .clone() + .unwrap_or_else(|| http_err.status.to_string()), + code: http_err.status.as_u16(), + data: None, + }, + None => ErrorPayload { + code: 500, + message: errr.to_string(), + data: None, + }, + }, + }; + + let body = serde_json::to_string(&payload).unwrap(); + let mut resp = Response::new(body.into()); + resp.headers_mut().insert( + http::header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + Err(resp) + } + } + } + } + impl MakeAxumError for &&ServerFnDecoder> { fn make_axum_error( self, diff --git a/packages/fullstack/src/payloads/websocket.rs b/packages/fullstack/src/payloads/websocket.rs index 6737adc36..3c65bb97f 100644 --- a/packages/fullstack/src/payloads/websocket.rs +++ b/packages/fullstack/src/payloads/websocket.rs @@ -61,7 +61,7 @@ use { pub fn use_websocket< In: 'static, Out: 'static, - E: Into + 'static, + E: Into + 'static, F: Future, E>> + 'static, Enc: Encoding, >( @@ -72,7 +72,7 @@ pub fn use_websocket< let status_read = use_hook(|| ReadSignal::new(status)); let connection = use_resource(move || { - let connection = connect_to_websocket().map_err(|e| CapturedError::from(e.into())); + let connection = connect_to_websocket().map_err(|e| e.into()); async move { let connection = connection.await; diff --git a/packages/fullstack/src/request.rs b/packages/fullstack/src/request.rs index 690a4a6af..8e214e202 100644 --- a/packages/fullstack/src/request.rs +++ b/packages/fullstack/src/request.rs @@ -149,7 +149,7 @@ impl std::future::Future for ServerFnRequest> { #[diagnostic::on_unimplemented( message = "The return type of a server function must be `Result`", note = "`T` is either `impl IntoResponse` *or* `impl Serialize`", - note = "`E` is either `From + Serialize`, `dioxus::Error` or `StatusCode`." + note = "`E` is either `From + Serialize`, `dioxus::CapturedError` or `StatusCode`." )] pub trait AssertIsResult {} impl AssertIsResult for Result {} diff --git a/packages/html/src/file_data.rs b/packages/html/src/file_data.rs index b5e5b5339..d67aee575 100644 --- a/packages/html/src/file_data.rs +++ b/packages/html/src/file_data.rs @@ -30,17 +30,18 @@ impl FileData { self.inner.last_modified() } - pub async fn read_bytes(&self) -> Result { + pub async fn read_bytes(&self) -> Result { self.inner.read_bytes().await } - pub async fn read_string(&self) -> Result { + pub async fn read_string(&self) -> Result { self.inner.read_string().await } pub fn byte_stream( &self, - ) -> Pin> + Send + 'static>> { + ) -> Pin> + Send + 'static>> + { self.inner.byte_stream() } @@ -70,13 +71,19 @@ pub trait NativeFileData: Send + Sync { fn content_type(&self) -> Option; fn read_bytes( &self, - ) -> Pin> + 'static>>; + ) -> Pin> + 'static>>; fn byte_stream( &self, - ) -> Pin> + 'static + Send>>; + ) -> Pin< + Box< + dyn futures_util::Stream> + + 'static + + Send, + >, + >; fn read_string( &self, - ) -> Pin> + 'static>>; + ) -> Pin> + 'static>>; fn inner(&self) -> &dyn std::any::Any; } @@ -143,7 +150,8 @@ mod serialize { fn read_bytes( &self, - ) -> Pin> + 'static>> { + ) -> Pin> + 'static>> + { let contents = self.contents.clone(); let path = self.path.clone(); @@ -157,13 +165,16 @@ mod serialize { return Ok(std::fs::read(path).map(Bytes::from)?); } - Err(dioxus_core::Error::msg("File contents not available")) + Err(dioxus_core::CapturedError::msg( + "File contents not available", + )) }) } fn read_string( &self, - ) -> Pin> + 'static>> { + ) -> Pin> + 'static>> + { let contents = self.contents.clone(); let path = self.path.clone(); @@ -177,7 +188,9 @@ mod serialize { return Ok(std::fs::read_to_string(path)?); } - Err(dioxus_core::Error::msg("File contents not available")) + Err(dioxus_core::CapturedError::msg( + "File contents not available", + )) }) } @@ -185,7 +198,9 @@ mod serialize { &self, ) -> Pin< Box< - dyn futures_util::Stream> + 'static + Send, + dyn futures_util::Stream> + + 'static + + Send, >, > { let contents = self.contents.clone(); @@ -201,7 +216,9 @@ mod serialize { return Ok(std::fs::read(path).map(Bytes::from)?); } - Err(dioxus_core::Error::msg("File contents not available")) + Err(dioxus_core::CapturedError::msg( + "File contents not available", + )) })) } diff --git a/packages/web/src/files.rs b/packages/web/src/files.rs index adb4bb418..75a3b2994 100644 --- a/packages/web/src/files.rs +++ b/packages/web/src/files.rs @@ -39,7 +39,7 @@ impl NativeFileData for WebFileData { fn read_bytes( &self, - ) -> Pin> + 'static>> { + ) -> Pin> + 'static>> { let file_reader = self.reader.clone(); let file_reader_ = self.reader.clone(); let file = self.file.clone(); @@ -72,7 +72,7 @@ impl NativeFileData for WebFileData { fn read_string( &self, - ) -> Pin> + 'static>> { + ) -> Pin> + 'static>> { let file_reader = self.reader.clone(); let file_reader_ = self.reader.clone(); let file = self.file.clone(); @@ -113,8 +113,13 @@ impl NativeFileData for WebFileData { /// We should maybe update these APIs to use our own custom `ByteBuffer` type to avoid going through `Vec`? fn byte_stream( &self, - ) -> Pin> + 'static + Send>> - { + ) -> Pin< + Box< + dyn futures_util::Stream> + + 'static + + Send, + >, + > { let file = self.file.dyn_ref::().unwrap().clone(); Box::pin(SendWrapper::new(futures_util::stream::once(async move { let array_buff = wasm_bindgen_futures::JsFuture::from(file.array_buffer())