Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/send-web-response-drain-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'vite-plugin-solid': patch
---

`sendWebResponse` no longer hangs forever when a client disconnects during backpressure. The write loop's `'drain'` wait had no other way to settle, but a response whose client already went away never emits `'drain'` — so every streamed SSR response aborted mid-stream (closed tab, slow mobile client) parked the promise chain, the body reader, and the Response object permanently, accumulating leaks over a turnkey dev/preview session. The backpressure wait now also settles on `'close'`/`'error'` and the loop bails out early once the response is destroyed, letting the existing close handler's reader cancellation finish cleanup.
21 changes: 20 additions & 1 deletion src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,27 @@ export async function sendWebResponse(res: ServerResponse, response: Response):
while (true) {
const { done, value } = await reader.read();
if (done) break;
// A response whose client already went away never emits 'drain'
// (writes are no-ops), so a backpressure wait must also settle on
// 'close'/'error' or an aborted streaming response parks this promise
// — and the reader and Response it holds — forever.
if (res.destroyed) return;
if (!res.write(value)) {
await new Promise((resolve) => res.once('drain', resolve));
const drained = await new Promise<boolean>((resolve) => {
const settle = (ok: boolean) => {
res.off('drain', onDrain);
res.off('close', onGone);
res.off('error', onGone);
resolve(ok);
};
const onDrain = () => settle(true);
const onGone = () => settle(false);
res.once('drain', onDrain);
res.once('close', onGone);
res.once('error', onGone);
});
// Client gone mid-stream; the 'close' handler cancels the reader.
if (!drained) return;
}
}
res.end();
Expand Down
Loading