Skip to content

Commit 2e7b63c

Browse files
brenelzclaude
andauthored
fix(http): sendWebResponse settles when the client disconnects during backpressure — the 'drain' wait had no other way to resolve, but a closed response never emits 'drain', so every streamed response aborted mid-stream leaked its promise chain, reader, and Response for the rest of the dev/preview session; the wait now races 'close'/'error' and the loop bails once the response is destroyed (#303)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 74fb28b commit 2e7b63c

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'vite-plugin-solid': patch
3+
---
4+
5+
`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.

src/http.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,27 @@ export async function sendWebResponse(res: ServerResponse, response: Response):
5656
while (true) {
5757
const { done, value } = await reader.read();
5858
if (done) break;
59+
// A response whose client already went away never emits 'drain'
60+
// (writes are no-ops), so a backpressure wait must also settle on
61+
// 'close'/'error' or an aborted streaming response parks this promise
62+
// — and the reader and Response it holds — forever.
63+
if (res.destroyed) return;
5964
if (!res.write(value)) {
60-
await new Promise((resolve) => res.once('drain', resolve));
65+
const drained = await new Promise<boolean>((resolve) => {
66+
const settle = (ok: boolean) => {
67+
res.off('drain', onDrain);
68+
res.off('close', onGone);
69+
res.off('error', onGone);
70+
resolve(ok);
71+
};
72+
const onDrain = () => settle(true);
73+
const onGone = () => settle(false);
74+
res.once('drain', onDrain);
75+
res.once('close', onGone);
76+
res.once('error', onGone);
77+
});
78+
// Client gone mid-stream; the 'close' handler cancels the reader.
79+
if (!drained) return;
6180
}
6281
}
6382
res.end();

0 commit comments

Comments
 (0)