From ad40f816073f91da67c0287a5512a5111e5a1617 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 1 Oct 2025 19:10:28 -0500 Subject: [PATCH] Fix the root error boundary during suspense in ssr (#4710) * Fix the root error boundary during suspense in ssr * fix some typos --- packages/cli/src/build/request.rs | 4 ++-- packages/core-types/src/formatter.rs | 2 +- packages/core/src/diff/node.rs | 4 ++-- packages/core/src/scopes.rs | 10 ++++++++-- .../playwright-tests/fullstack-routing.spec.js | 13 +++++++++++++ .../fullstack-routing/src/main.rs | 17 +++++++++++++++++ packages/server/src/render.rs | 2 +- packages/subsecond/subsecond/src/lib.rs | 2 +- packages/wasm-split/wasm-split-cli/src/lib.rs | 8 ++++---- 9 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index cd85d7929..8e9558386 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -2754,7 +2754,7 @@ impl BuildRequest { ), ("OPENSSL_LIBS".to_string(), openssl_libs.into()), // Set the wry env vars - this is where wry will dump its kotlin files. - // Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry. + // Their setup is really annoying and requires us to hardcode `dx` to specific versions of tao/wry. ( "WRY_ANDROID_PACKAGE".to_string(), "dev.dioxus.main".to_string().into(), @@ -3926,7 +3926,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ } } - /// Get the path to the wasm-bindgen output files. Either the direct file or the opitmized one depending on the build mode + /// Get the path to the wasm-bindgen output files. Either the direct file or the optimized one depending on the build mode fn bundled_wasm_path(&self, assets: &AssetManifest) -> String { let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file(); if self.should_bundle_to_asset() { diff --git a/packages/core-types/src/formatter.rs b/packages/core-types/src/formatter.rs index a4df24372..ed13dbefb 100644 --- a/packages/core-types/src/formatter.rs +++ b/packages/core-types/src/formatter.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; /// Take this type and format it into a Cow<'static, str> /// -/// This trait exists so libraries like manganis can implement this type for asssets without depending +/// This trait exists so libraries like manganis can implement this type for assets without depending /// on dioxus-core, which can be heavy in proc macros and build scripts. /// /// We don't want a blanket impl for T: Display because that might conflict for the other integral data diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs index e239fcb48..bb1cae203 100644 --- a/packages/core/src/diff/node.rs +++ b/packages/core/src/diff/node.rs @@ -795,7 +795,7 @@ impl VNode { fn write_attrs( &self, mount: MountId, - dynamic_attrbiutes_iter: &mut Peekable>, + dynamic_attributes_iter: &mut Peekable>, root_idx: u8, dom: &mut VirtualDom, to: &mut impl WriteMutations, @@ -804,7 +804,7 @@ impl VNode { // Only take nodes that are under this root node let from_root_node = |(_, path): &(usize, &[u8])| path.first() == Some(&root_idx); while let Some((attribute_idx, attribute_path)) = - dynamic_attrbiutes_iter.next_if(from_root_node) + dynamic_attributes_iter.next_if(from_root_node) { let attribute = &self.dynamic_attrs[attribute_idx]; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 6d2807841..02e4260df 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -45,11 +45,17 @@ impl ScopeId { /// let my_persistent_state = Signal::new_in_scope(String::new(), ScopeId::APP); /// ``` // ScopeId(0) is the root scope wrapper - // ScopeId(1) is the default error boundary - // ScopeId(2) is the default suspense boundary + // ScopeId(1) is the default suspense boundary + // ScopeId(2) is the default error boundary // ScopeId(3) is the users root scope pub const APP: ScopeId = ScopeId(3); + /// The ScopeId of the topmost error boundary in the tree. + pub const ROOT_ERROR_BOUNDARY: ScopeId = ScopeId(2); + + /// The ScopeId of the topmost suspense boundary in the tree. + pub const ROOT_SUSPENSE_BOUNDARY: ScopeId = ScopeId(1); + /// The ScopeId of the topmost scope in the tree. /// This will be higher up in the tree than [`ScopeId::APP`] because dioxus inserts a default [`crate::SuspenseBoundary`] and [`crate::ErrorBoundary`] at the root of the tree. // ScopeId(0) is the root scope wrapper diff --git a/packages/playwright-tests/fullstack-routing.spec.js b/packages/playwright-tests/fullstack-routing.spec.js index 5ae3043f1..2e907a0ca 100644 --- a/packages/playwright-tests/fullstack-routing.spec.js +++ b/packages/playwright-tests/fullstack-routing.spec.js @@ -42,6 +42,19 @@ test("error route", async ({ request }) => { expect(response.status()).toBe(500); }); +// The async error route should return 500 +test("async error route", async ({ request }) => { + await waitForBuild(request); + const response = await request.get("http://localhost:8888/async-error"); + + expect(response.status()).toBe(500); + + // Expect the response to contain the error message + const errorMessage = "Async error from a server function"; + const text = await response.text(); + expect(text).toContain(errorMessage); +}); + // An unknown route should return 404 test("unknown route", async ({ request }) => { await waitForBuild(request); diff --git a/packages/playwright-tests/fullstack-routing/src/main.rs b/packages/playwright-tests/fullstack-routing/src/main.rs index 12ae64068..19588081a 100644 --- a/packages/playwright-tests/fullstack-routing/src/main.rs +++ b/packages/playwright-tests/fullstack-routing/src/main.rs @@ -30,6 +30,9 @@ enum Route { #[route("/error")] ThrowsError, + #[route("/async-error")] + ThrowsAsyncError, + #[route("/can-go-back")] HydrateCanGoBack, } @@ -52,6 +55,20 @@ fn ThrowsError() -> Element { ))); } +#[component] +fn ThrowsAsyncError() -> Element { + #[server] + async fn error_after_delay() -> ServerFnResult<()> { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + Err(ServerFnError::new("Async error from a server function")) + } + + use_server_future(error_after_delay)?().unwrap()?; + rsx! { + "Hello, world!" + } +} + #[component] fn Home() -> Element { let route: Route = use_route(); diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 8c316ddc7..4ee870726 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -241,7 +241,7 @@ impl SsrRendererPool { // check if there are any errors let errors = virtual_dom.in_runtime(|| { - let error_context: ErrorContext = ScopeId::APP + let error_context: ErrorContext = ScopeId::ROOT_ERROR_BOUNDARY .consume_context() .expect("The root should be under an error boundary"); let errors = error_context.errors(); diff --git a/packages/subsecond/subsecond/src/lib.rs b/packages/subsecond/subsecond/src/lib.rs index 40eb5d4bc..d43044150 100644 --- a/packages/subsecond/subsecond/src/lib.rs +++ b/packages/subsecond/subsecond/src/lib.rs @@ -499,7 +499,7 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> { // On non-wasm platforms we can just use libloading and the known aslr offsets to load the library #[cfg(any(unix, windows))] { - // on android we try to cirumvent permissions issues by copying the library to a memmap and then libloading that + // on android we try to circumvent permissions issues by copying the library to a memmap and then libloading that #[cfg(target_os = "android")] let lib = Box::leak(Box::new(android_memmap_dlopen(&table.lib)?)); diff --git a/packages/wasm-split/wasm-split-cli/src/lib.rs b/packages/wasm-split/wasm-split-cli/src/lib.rs index bcf9d347f..e8338ed66 100644 --- a/packages/wasm-split/wasm-split-cli/src/lib.rs +++ b/packages/wasm-split/wasm-split-cli/src/lib.rs @@ -262,7 +262,7 @@ impl<'a> Splitter<'a> { self.clear_data_segments(&mut out, &unique_symbols); // Clear out the element segments and then add in the initializers for the shared imports - self.create_ifunc_initialzers(&mut out, &unique_symbols); + self.create_ifunc_initializers(&mut out, &unique_symbols); // Convert our split module's functions to real functions that call the indirect function self.add_split_imports( @@ -332,7 +332,7 @@ impl<'a> Splitter<'a> { self.clear_data_segments(&mut out, &unique_symbols); // Clear out the element segments and then add in the initializers for the shared imports - self.create_ifunc_initialzers(&mut out, &unique_symbols); + self.create_ifunc_initializers(&mut out, &unique_symbols); // We have to make sure our table matches that of the other tables even though we don't call them. let ifunc_table_id = self.load_funcref_table(&mut out); @@ -554,7 +554,7 @@ impl<'a> Splitter<'a> { } /// Creates the jump points - fn create_ifunc_initialzers(&self, out: &mut Module, unique_symbols: &HashSet) { + fn create_ifunc_initializers(&self, out: &mut Module, unique_symbols: &HashSet) { let ifunc_table = self.load_funcref_table(out); let mut initializers = HashMap::new(); @@ -1009,7 +1009,7 @@ impl<'a> Splitter<'a> { let mut recovered_children = HashSet::new(); for lost in lost_children { match lost { - // Functions need to be found - the wasm decsribe functions are usually completely dissolved + // Functions need to be found - the wasm describe functions are usually completely dissolved Node::Function(id) => { let func = original.module.funcs.get(id); let name = func.name.as_ref().unwrap();