Skip to content

fix(utils): do not chain a promise per chunk while streaming a response - #417

Open
TonyTech404 wants to merge 1 commit into
honojs:mainfrom
TonyTech404:fix/stream-write-promise-chain
Open

TonyTech404 wants to merge 1 commit into
honojs:mainfrom
TonyTech404:fix/stream-write-promise-chain

Conversation

@TonyTech404

Copy link
Copy Markdown

Fixes #407

Problem

In writeFromReadableStreamDefaultReader, flow returns the promise of the next read from inside a then callback. A promise returned from a then callback makes the promise of that then wait for it, so while write() keeps returning true each chunk adds one pending promise to a chain. The chain only settles when the stream ends, write() returns false, or an error occurs. For a paced stream that never fills the socket (live audio, SSE) the number of live promises, and the memory, grows without bound while the response is open.

The analysis and the one-line fix come from @Mheaus in #407, who also supplied a reproduction. I reproduced it independently and wrote the regression test below.

Fix

Start the next read without returning its promise. Nothing uses the value flow returns (it is only passed to then), and errors still reach handleStreamError, so behaviour is otherwise unchanged. The return type of flow becomes void.

Verification

  • New test in test/utils.test.ts: a paced ReadableStream is written to a Writable whose write() always returns true, and the number of live Promise objects (v8.queryObjects) is sampled at two points of the same open stream. Without the fix the test fails with expected 899 to be less than 100; with it, it passes.
  • pnpm format, pnpm lint and pnpm typecheck pass.
  • Full test suite passes on Node.js 20.20.2. On Node.js 24.22.2 and 26.4.0 there are failures in test/server.test.ts (GET /readable-stream) and test/request.test.ts (stream closes mid-read), which also fail on an unmodified main; they are unrelated to this change.
  • v8.queryObjects prints an ExperimentalWarning once during the test run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A paced streaming response keeps one Promise for each chunk while write() returns true

1 participant