Skip to content

Commit d891b1d

Browse files
committed
fix: make sure promises from fetch handle errors
Ensures that people using `fetch` directly in their load functions don't run into uncaught promise errors with streaming. Also adds some docs for this case. related to #9785
1 parent e062261 commit d891b1d

File tree

7 files changed

+78
-2
lines changed

7 files changed

+78
-2
lines changed

.changeset/soft-colts-tell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: make sure promises from fetch handle errors

documentation/docs/20-core-concepts/20-load.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,25 @@ This is useful for creating skeleton loading states, for example:
488488
</p>
489489
```
490490

491+
When streaming data, be careful to handle promise rejections correctly. More specifically, the server could crash with an "unhandled promise rejection" error if a lazy-loaded promise fails before rendering starts (at which point it's caught) and isn't handling the error in some way. When using SvelteKit's `fetch` directly in the `load` function, SvelteKit will handle this case for you. For other promises, it is enough to attach a noop-`catch` to the promise to mark it as handled.
492+
493+
```js
494+
/// file: src/routes/+page.server.js
495+
/** @type {import('./$types').PageServerLoad} */
496+
export function load({ fetch }) {
497+
const ok_manual = Promise.reject();
498+
ok_manual.catch(() => {});
499+
500+
return {
501+
streamed: {
502+
ok_manual,
503+
ok_fetch: fetch('/fetch/that/could/fail'),
504+
dangerous_unhandled: Promise.reject()
505+
}
506+
};
507+
}
508+
```
509+
491510
> On platforms that do not support streaming, such as AWS Lambda, responses will be buffered. This means the page will only render once all promises resolve. If you are using a proxy (e.g. NGINX), make sure it does not buffer responses from the proxied server.
492511
493512
> Streaming data will only work when JavaScript is enabled. You should avoid returning nested promises from a universal `load` function if the page is server rendered, as these are _not_ streamed — instead, the promise is recreated when the function reruns in the browser.

packages/kit/src/runtime/server/fetch.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import * as paths from '__sveltekit/paths';
1414
* @returns {typeof fetch}
1515
*/
1616
export function create_fetch({ event, options, manifest, state, get_cookie_header, set_internal }) {
17-
return async (info, init) => {
17+
// Don't make this function `async`! Otherwise, the user has to `catch` promises they use for streaming responses or else
18+
// it will be an unhandled rejection. Instead, we add a `.catch(() => {})` ourselves below to keep this from happening.
19+
return (info, init) => {
1820
const original_request = normalize_fetch_input(info, init, event.url);
1921

2022
// some runtimes (e.g. Cloudflare) error if you access `request.mode`,
@@ -23,7 +25,7 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
2325
let credentials =
2426
(info instanceof Request ? info.credentials : init?.credentials) ?? 'same-origin';
2527

26-
return await options.hooks.handleFetch({
28+
const response = options.hooks.handleFetch({
2729
event,
2830
request: original_request,
2931
fetch: async (info, init) => {
@@ -143,6 +145,14 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
143145
return response;
144146
}
145147
});
148+
149+
// See above for an explanation
150+
if ('catch' in response && typeof response.catch === 'function') {
151+
response.catch(() => {});
152+
return response;
153+
} else {
154+
return Promise.resolve(response);
155+
}
146156
};
147157
}
148158

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
<a href="/streaming/universal">Universal</a>
22
<a href="/streaming/server">Server</a>
3+
<a href="/streaming/server-error">Server Error</a>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Tests the case where a lazy promise is rejected before the rendering started
2+
export async function load({ fetch }) {
3+
const eager = new Promise((resolve) => {
4+
setTimeout(() => {
5+
resolve('eager');
6+
}, 100);
7+
});
8+
9+
return {
10+
eager: await eager,
11+
lazy: {
12+
fail: fetch('http://localhost:1337/')
13+
}
14+
};
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
/** @type {import('./$types').PageData} */
3+
export let data;
4+
</script>
5+
6+
<p class="eager">{data.eager}</p>
7+
8+
{#await data.lazy.fail}
9+
<p class="loadingfail">loading</p>
10+
{:catch}
11+
<p class="fail">fail</p>
12+
{/await}

packages/kit/test/apps/basics/test/client.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,14 @@ test.describe('Streaming', () => {
761761
expect(page.locator('p.loadingfail')).toBeHidden();
762762
});
763763

764+
test('Catches fetch errors from server load functions (client nav)', async ({ page }) => {
765+
await page.goto('/streaming');
766+
page.click('[href="/streaming/server-error"]');
767+
768+
await expect(page.locator('p.eager')).toHaveText('eager');
769+
expect(page.locator('p.fail')).toBeVisible();
770+
});
771+
764772
// TODO `vite preview` buffers responses, causing these tests to fail
765773
if (process.env.DEV) {
766774
test('Works for universal load functions (direct hit)', async ({ page }) => {
@@ -802,6 +810,12 @@ test.describe('Streaming', () => {
802810
expect(page.locator('p.loadingsuccess')).toBeHidden();
803811
expect(page.locator('p.loadingfail')).toBeHidden();
804812
});
813+
814+
test('Catches fetch errors from server load functions (direct hit)', async ({ page }) => {
815+
page.goto('/streaming/server-error');
816+
await expect(page.locator('p.eager')).toHaveText('eager');
817+
await expect(page.locator('p.fail')).toHaveText('fail');
818+
});
805819
}
806820
});
807821

0 commit comments

Comments
 (0)