Fix the root error boundary during suspense in ssr (#4710)

* Fix the root error boundary during suspense in ssr

* fix some typos
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-10-01 17:10:28 -07:00
1 parent 10912f4750
commit ad40f81607
9 files changed
+49 -13

No files matched your search

+2 -2
View File
@@ -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() {
+1 -1
View File
@@ -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
+2 -2
View File
@@ -795,7 +795,7 @@ impl VNode {
fn write_attrs(
&self,
mount: MountId,
dynamic_attrbiutes_iter: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
dynamic_attributes_iter: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
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];
+8 -2
View File
@@ -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
@@ -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);
@@ -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();
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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)?));
@@ -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<Node>) {
fn create_ifunc_initializers(&self, out: &mut Module, unique_symbols: &HashSet<Node>) {
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();