Merge pull request #338 from benwis/error-handling

ErrorBoundary Component
This commit is contained in:
Greg Johnston 2023-01-21 16:03:48 -05:00 committed by GitHub
commit a5531b1a7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 199 additions and 37 deletions

View file

@ -0,0 +1,28 @@
use leptos::Errors;
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {cx,
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.get().0.into_iter()}
// a unique key for each item as a reference
key=|error| error.0.clone()
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view(cx)
}

View file

@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::LeptosOptions;
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await?;
let res = get_static_file(uri.clone(), &root).await.unwrap();
match res.status() {
StatusCode::OK => Ok(res),
_ => Err((res.status(), "File Not Found".to_string()))
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
handler(req).await.into_response()
}
}
@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
)),
}
}
}
}

View file

@ -3,7 +3,8 @@ use leptos::{component, view, IntoView, Scope};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod file;
pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::nav::*;

View file

@ -11,7 +11,7 @@ if #[cfg(feature = "ssr")] {
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use hackernews_axum::file::file_handler;
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
async fn main() {
@ -26,9 +26,9 @@ if #[cfg(feature = "ssr")] {
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_handler))
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.fallback(file_handler)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper

View file

@ -38,17 +38,7 @@ sqlx = { version = "0.6.2", features = [
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"leptos_axum",
]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "leptos_axum"]
[package.metadata.cargo-all-features]
denylist = [
@ -103,4 +93,4 @@ lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-default-features = false

View file

@ -28,7 +28,7 @@ cargo leptos build --release
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"pkg"`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack

View file

@ -0,0 +1,28 @@
use leptos::Errors;
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {cx,
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.get().0.into_iter()}
// a unique key for each item as a reference
key=|error| error.0.clone()
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view(cx)
}

View file

@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::LeptosOptions;
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await?;
let res = get_static_file(uri.clone(), &root).await.unwrap();
match res.status() {
StatusCode::OK => Ok(res),
_ => Err((res.status(), "File Not Found".to_string()))
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
handler(req).await.into_response()
}
}
@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
)),
}
}
}
}

View file

@ -1,6 +1,7 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod file;
pub mod error_template;
pub mod fallback;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.

View file

@ -10,7 +10,7 @@ if #[cfg(feature = "ssr")] {
};
use crate::todo::*;
use todo_app_sqlite_axum::*;
use crate::file::file_handler;
use crate::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
@ -36,7 +36,7 @@ if #[cfg(feature = "ssr")] {
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
.fallback(file_handler)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper

View file

@ -120,8 +120,9 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
<Routes>
<Route path="" view=|cx| view! {
cx,
<Todos/>
<Todos/>
}/>
</Routes>
</main>
</Router>

View file

@ -0,0 +1,27 @@
use leptos_dom::{Errors, Fragment, IntoView, View};
use leptos_macro::component;
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
#[component(transparent)]
pub fn ErrorBoundary<F>(
cx: Scope,
/// The components inside the tag which will get rendered
children: Box<dyn FnOnce(Scope) -> Fragment>,
/// A fallback that will be shown if an error occurs.
fallback: F,
) -> impl IntoView
where
F: Fn(Scope, Option<RwSignal<Errors>>) -> View + 'static,
{
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
provide_context(cx, errors);
// Run children so that they render and execute resources
let children = children(cx);
move || match errors.get().0.is_empty() {
true => children.clone(),
false => fallback(cx, Some(errors)).into(),
}
}

View file

@ -138,7 +138,8 @@ pub use leptos_server::*;
pub use tracing;
pub use typed_builder;
mod error_boundary;
pub use error_boundary::*;
mod for_loop;
pub use for_loop::*;
mod suspense;

View file

@ -1,5 +1,6 @@
mod dyn_child;
mod each;
mod errors;
mod fragment;
mod unit;
@ -11,6 +12,7 @@ use crate::{
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
pub use dyn_child::*;
pub use each::*;
pub use errors::*;
pub use fragment::*;
use leptos_reactive::Scope;
#[cfg(all(target_arch = "wasm32", feature = "web"))]

View file

@ -0,0 +1,71 @@
use crate::{HydrationCtx, HydrationKey, IntoView};
use cfg_if::cfg_if;
use leptos_reactive::{use_context, RwSignal};
use std::{collections::HashMap, error::Error, rc::Rc};
/// A struct to hold all the possible errors that could be provided by child Views
#[derive(Debug, Clone, Default)]
pub struct Errors(pub HashMap<HydrationKey, Rc<dyn Error>>);
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: std::error::Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
match self {
Ok(stuff) => stuff.into_view(cx),
Err(error) => {
match use_context::<RwSignal<Errors>>(cx) {
Some(errors) => {
let id = HydrationCtx::id();
errors.update({
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(any(feature = "hydrate", feature="csr"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
});
});
});
}
}
}
None => {
#[cfg(debug_assertions)]
warn!(
"No ErrorBoundary components found! Returning errors will not \
be handled and will silently disappear"
);
}
}
().into_view(cx)
}
}
}
}
impl Errors {
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
where
E: Error + 'static,
{
self.0.insert(key, Rc::new(error));
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove<E>(&mut self, key: &HydrationKey)
where
E: Error + 'static,
{
self.0.remove(key);
}
}

View file

@ -49,7 +49,7 @@ cfg_if! {
}
/// A stable identifer within the server-rendering or hydration process.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct HydrationKey {
/// The key of the previous component.
pub previous: String,