Streaming Promises on a +page.server file misbehave in a built environment
Describe the bug
Streamed Promises work correctly in the development environment (npm run dev), but block the page load in a built environment (For example npm run build && npm run preview)
I'm using the latest version of sveltekit
Reproduction
- Install a new sveltekit app
npm create svelte@latest my-app - Install dependencies
npm install - Create a src/routes/+page.svelte file with the example from the SvelteKit docs
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>
<p>
one: {data.one}
</p>
<p>
two: {data.two}
</p>
<p>
three:
{#await data.streamed.three}
Loading...
{:then value}
{value}
{:catch error}
{error.message}
{/await}
</p>
- Create a src/routes/+page.server.js with the example in the SvelteKit docs
/** @type {import('./$types').PageServerLoad} */
export function load() {
return {
one: Promise.resolve(1),
two: Promise.resolve(2),
streamed: {
three: new Promise((fulfil) => {
setTimeout(() => {
fulfil(3)
}, 1000);
})
}
};
}
- Run the dev environment
npm run devand load the dev page at http://localhost:5174/ (adjust the port if needed) Enjoy the "loading..." message for a second and then enjoy the final "3" - Now build the app and preview it,
npm run build && npm run previewand load the page at http://localhost:4173/ (adjust the port if needed) The page blocks for a second, shows "loading..." for an instant and then shows the final "3"
Logs
No response
System Info
System:
OS: Linux 6.1 Fedora Linux 37 (Workstation Edition)
CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
Memory: 4.74 GB / 15.35 GB
Container: Yes
Shell: 5.2.15 - /bin/bash
Binaries:
Node: 18.15.0 - /usr/bin/node
npm: 9.5.0 - /usr/bin/npm
Browsers:
Firefox: 111.0
npmPackages:
@sveltejs/adapter-auto: ^2.0.0 => 2.0.0
@sveltejs/kit: ^1.5.0 => 1.14.0
svelte: ^3.54.0 => 3.57.0
vite: ^4.2.0 => 4.2.1
Severity
annoyance
Additional Information
No response
The problem is somewhere within the node stream or the browser itself. We're calling res.write(chunk) but that does not flush out the chunk immediately. Instead it seems to wait for more data and flush everything once res.end() is called. When doing curl localhost:4173 the first data chunk shows up immediately, which makes me think it's the browser. I'm not sure how to get it to flush out the data eagerly, other than appending junk data to the chunk which pushes it over the threshold.
I have also encountered this problem and would like to help find a fix. Maybe this helps:
| Command | Chrome/Firefox | curl 7.87.0 |
|---|---|---|
vite dev
| ✅ | ✅ |
vite preview
| ❌ | ✅ |
node build locally
| ✅ | ✅ |
node build in prod, behind nginx
| ❌ | ✅ but chunks in a weird place inside the SvelteKit <script> tag – BEFORE the </html>
|
It feels like there might be several factors at play here? I don't understand why npm run preview does not work, but node build DOES, except that behind nginx, it doesn't. Can I try anything else to help get to the bottom of this?
(I now realize that my nginx problems are not what this issue was originally about, but I still wanted to follow up)
I looked into this a little more and I came to the conclusion that vite preview not working is probably unrelated (and might even be considered "correct" if you're using adapters/platforms that don't support streaming.) vite preview seems to use a different setup from what you're getting out of adapter-node.
I was still unsure why streaming didn't work for me on production, behind nginx. This seems to be a feature of nginx: By default, nginx buffers the response from the proxied server, so the streaming does not reach the client. (Except if the buffer fills up.)
This gets amplified when nginx also gzips the responses, because then we have another buffer. (this also explains why __data.json seemed to work better for me: I didn't gzip the content type text/sveltekit-data.)
If I turn the proxy_buffering setting off, streaming responses immediately work:
# nginx.conf
location / {
proxy_buffering off;
# ...
}
But I think this may be a bad idea, so I'm not sure what I'm going to do.
@danieldiekmeier you can disable NGINX proxy buffer by set X-Accel-Buffering header on specific route:
export async function load({ setHeaders }) {
setHeaders({
'X-Accel-Buffering': 'no'
});
...
}
NGINX Docs: nginx.org/en/docs/http/ngx_http_proxy_module
(I now realize that my nginx problems are not what this issue was originally about, but I still wanted to follow up)
I looked into this a little more and I came to the conclusion that
vite previewnot working is probably unrelated (and might even be considered "correct" if you're using adapters/platforms that don't support streaming.)vite previewseems to use a different setup from what you're getting out ofadapter-node.I was still unsure why streaming didn't work for me on production, behind nginx. This seems to be a feature of nginx: By default, nginx buffers the response from the proxied server, so the streaming does not reach the client. (Except if the buffer fills up.)
This gets amplified when nginx also gzips the responses, because then we have another buffer. (this also explains why
__data.jsonseemed to work better for me: I didn't gzip the content typetext/sveltekit-data.)If I turn the
proxy_bufferingsetting off, streaming responses immediately work:# nginx.conf location / { proxy_buffering off; # ... }But I think this may be a bad idea, so I'm not sure what I'm going to do.
In the end I was only able to get streaming working with "proxy_buffering off;" Also, it still doesn't work locally when running:
npm run build then npm run preview
I'm also experiencing this issue. It runs fine on the dev server, but not once built, including when deployed to Vercel. I'm using:
@sveltejs/kit: v2.5.8@sveltejs/adapter-vercel: v5.3.0
There are workarounds, but they're not at all ideal.