kit: Rejecting streamed data

Describe the bug

When rejecting promises for streamed data, dev server usually crashes, and when it doesn’t crash it doesn’t show my custom error message.

Reproduction

https://stackblitz.com/edit/sveltejs-kit-template-default-2dz1kb?file=src/routes/+page.server.js

Logs

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "Not Found".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

System Info

System:
    OS: Linux 5.19 Ubuntu 22.04.2 LTS 22.04.2 LTS (Jammy Jellyfish)
    CPU: (12) x64 AMD Ryzen 5 5600X 6-Core Processor
    Memory: 7.30 GB / 15.53 GB
    Container: Yes
    Shell: 5.8.1 - /usr/bin/zsh
  Binaries:
    Node: 18.15.0 - ~/.nvm/versions/node/v18.15.0/bin/node
    npm: 9.5.0 - ~/.nvm/versions/node/v18.15.0/bin/npm
  Browsers:
    Firefox: 112.0.1
  npmPackages:
    @sveltejs/adapter-node: ^1.2.2 => 1.2.3 
    @sveltejs/kit: ^1.15.7 => 1.15.7 
    svelte: ^3.58.0 => 3.58.0 
    vite: ^4.3.1 => 4.3.1

Severity

serious, but I can work around it

Additional Information

No response

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 14
  • Comments: 21 (7 by maintainers)

Commits related to this issue

Most upvoted comments

This issue makes streamed data mostly unusable. Especially when dealing with fetches that may return a 404 or those that want to throw a redirect. It, for example, prevents us from using redirects to protect authentication sensitive routes.

And whats even worse is that the documentation of streamed data is suggesting that error handling isn’t an issue. The example promise won’t fail

            three: new Promise((fulfil) => {
                setTimeout(() => {
                    fulfil(3)
                }, 1000);
            })

and the error handling

    {#await data.streamed.three}
        Loading...
    {:then value}
        {value}
    {:catch error}
        {error.message}
    {/await}

suggests that rejected, streamed data does not crash the server.

Debugging this issue has cost us a lot of time that could have been prevent by mentioning this caveat in the documentation, or even better, implementing a fix. This should be a high priority issue, if you ask me.

@gtm-nayan - I think if this can’t be done for now, you guys need to update the docs explaining this clearly perhaps with an example workaround. If the await catch doesn’t work in the template at all, this shouldn’t be the example listed.

J

We’ll probably not be able to fix this without changing a few things in a major version because your promises can reject before they even reach SvelteKit.

In the meantime you can use workarounds https://jakearchibald.com/2023/unhandled-rejections/ to prevent crashing your server. Or set up a global promise rejection handler, which would be a good idea regardless of streamed promises.

For the ones that want to throw redirects from their streamed promises, that will not work, ever, because the browser has already received an OK response with the rest of the page by that point. The best you can do there is make another navigation on the client side like @eliasdefaria said.

We’ll probably not be able to fix this without changing a few things in a major version because your promises can reject before they even reach SvelteKit.

In the meantime you can use workarounds https://jakearchibald.com/2023/unhandled-rejections/ to prevent crashing your server. Or set up a global promise rejection handler, which would be a good idea regardless of streamed promises.

For the ones that want to throw redirects from their streamed promises, that will not work, ever, because the browser has already received an OK response with the rest of the page by that point. The best you can do there is make another navigation on the client side like @eliasdefaria said.

Thank you for the workaround, but I am here from my first Svelte (and JS) project, and that seems a bit complex for me.

A friend helped and I got this working. Hope it helps some newer people like me who want to use streaming promises without having the whole app crash and need a more basic workaround.

in +page.server.ts

export const load: PageServerLoad = async ({ params, locals }) => {
	const res = fetch(`http://localhost:8000/api/myendpoint`);
	return { results: { streamed: res
			.then(
				(resp) => {
					if (resp.status === 200) {
						return resp.json()
					} else if (resp.status === 404) {
						console.log("App Not found");
						return "Not Found"
					} else if (resp.status === 500) {
						console.log("API Server error");
						return "Backend Error"
					}
				}
			)
			.then(
				json=> json,
				error => {console.log('Uncaught error', error); return "Uncaught Error"}
			)
		}};
};

Then in my +page.svelte

{#await data.results.streamed}
	Loading ...
{:then results}
	{#if typeof results != 'string'}
		Loaded!
		<!-- <AppGroupCard apps={results} /> -->
	{:else}
		<p>Search failed please try again ... {results}</p>
	{/if}
{:catch error}
	<!-- NOTE: This is currently not displaying -->
	<p>Search failed please try again ... {error.message}</p>
{/await}

Adding this snippet to the bottom of svelte.config.js will prevent crashes and help with debugging info during development

process.on('unhandledRejection', (reason, promise) => {
	console.error('Unhandled Rejection at: Promise', promise, 'reason:', reason);
});

this is what I am using to resolve this issue

function streamedError(err: {status: number, body: {message: string}}) { return new Promise((fulfil, reject) => { setTimeout(() => { reject(error(err.status, err.body.message)); }, 500); }); }

and then I attach it to the streamed promises as follows: streamedFunc().catch(err=>streamedError(err))

@adrifer Before you tread down that path, I should let you know that you can’t throw redirects from a streamed promise. The server needs to know the status code before it starts sending the response and it’s too late by the time it gets to your streamed promises.

As for the issue in general, Node.js requires that an error handler must be attached before the tick in which the promise is rejected. In SvelteKit, that’s done by the serializer, but by the time we’re done awaiting all the load functions it may have been too late to attach the handler.

I can’t think of a clean way to solve this yet, I don’t think it’s actually possible with the current API, we’d have to walk the returned objects (arrays, maps, whatever included) as soon as we get it from the user but turns out even that’s not enough.

Consider

{ nested: { promise: Promise.reject() }, something: await setTimeout(1000) }

the rejected promise is created before the yield point of the timeout so the exception is triggered and the process exits before the promise even reaches SvelteKit.

Perhaps there is some way to kludge this, ensuring we attach the error handlers before any other yield point is reached. ~The handlers would catch the error and put it back on the promise under some special key and the serializer checks the key to get errors that happened before the promise reached the serializer~ (apparently just need to chain a catch handler, and not use the chained promise) but I won’t be too hopeful that it’s viable. We might need something like defer() after all.

Cannot reproduce the crash with the provided repro on either stackblitz or locally, but can confirm the error reporting seems to be broken. It’s logging undefined every time.