mirror of https://github.com/sveltejs/svelte
fix: call `onerror` and provide a working `reset` when hydrating a failed boundary (#18556)
Fixes #18555 A boundary that failed during SSR hydrates via `#hydrate_failed_content`, which never calls `onerror` and passes the `failed` snippet a no-op `reset`. Both came in with #17672, whose docs say `onerror` "will be called upon hydration with the deserialized error object". Once hydrated as failed, the boundary can never leave that state. Downstream this is what keeps SvelteKit's `+error.svelte` mounted after navigating away from a server-rendered error page (sveltejs/kit#16345). This extracts the reset/onerror machinery from `#handle_error` into `#create_reset` and uses it in the hydration path too. `onerror` is invoked in a microtask because it may mutate state, which is disallowed while hydrating. `#handle_error` already invokes it asynchronously, so the timing matches the error path. The tests flip a `recovered` flag before calling `reset`, since the child would otherwise throw again. That is the intended retry pattern, and the same one kit uses when it resets route boundaries on navigation. Verified against kit end to end, hydrating a server-rendered error page and navigating away now tears it down with no kit changes needed.pull/18197/merge
parent
2bace308e3
commit
3dde011d3a
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: call `onerror` and provide a working `reset` when hydrating a failed boundary
|
||||
@ -0,0 +1,19 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['hydrate'],
|
||||
ssrHtml: '<p>failed: error</p> <button>reset</button>',
|
||||
transformError: () => 'error',
|
||||
|
||||
test({ assert, target, logs }) {
|
||||
// `onerror` is called upon hydration with the deserialized error
|
||||
assert.deepEqual(logs, ['onerror: error']);
|
||||
|
||||
const btn = target.querySelector('button');
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, '<p>recovered</p> <button>reset</button>');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
<script>
|
||||
throw new Error('unlucky');
|
||||
</script>
|
||||
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import Child from './child.svelte';
|
||||
|
||||
let recovered = $state(false);
|
||||
let reset_fn = $state();
|
||||
</script>
|
||||
|
||||
<svelte:boundary
|
||||
onerror={(error, reset) => {
|
||||
console.log(`onerror: ${error}`);
|
||||
reset_fn = reset;
|
||||
}}
|
||||
>
|
||||
{#if recovered}
|
||||
<p>recovered</p>
|
||||
{:else}
|
||||
<Child />
|
||||
{/if}
|
||||
|
||||
{#snippet failed(error)}
|
||||
<p>failed: {error}</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
||||
<button
|
||||
onclick={() => {
|
||||
recovered = true;
|
||||
reset_fn();
|
||||
}}>reset</button>
|
||||
@ -0,0 +1,16 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['hydrate'],
|
||||
ssrHtml: '<p>failed: error</p> <button>reset</button>',
|
||||
transformError: () => 'error',
|
||||
|
||||
test({ assert, target }) {
|
||||
const btn = target.querySelector('button');
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, '<p>recovered</p>');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
<script>
|
||||
throw new Error('unlucky');
|
||||
</script>
|
||||
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import Child from './child.svelte';
|
||||
|
||||
let recovered = $state(false);
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
{#if recovered}
|
||||
<p>recovered</p>
|
||||
{:else}
|
||||
<Child />
|
||||
{/if}
|
||||
|
||||
{#snippet failed(error, reset)}
|
||||
<p>failed: {error}</p>
|
||||
<button
|
||||
onclick={() => {
|
||||
recovered = true;
|
||||
reset();
|
||||
}}>reset</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
Loading…
Reference in new issue