Skip to content

stream: fix TypeError in writev when writer.ready rejects#62291

Open
mango766 wants to merge 1 commit intonodejs:mainfrom
mango766:fix/webstreams-writev-unhandled-rejection
Open

stream: fix TypeError in writev when writer.ready rejects#62291
mango766 wants to merge 1 commit intonodejs:mainfrom
mango766:fix/webstreams-writev-unhandled-rejection

Conversation

@mango766
Copy link

The done callback in the writev handlers of both
newStreamWritableFromWritableStream and
newStreamDuplexFromReadableWritablePair unconditionally called
error.filter(), assuming error is always an array (from
SafePromiseAll). However, done is also used as the rejection
handler for writer.ready, which passes a single error value,
not an array. This caused a TypeError: Cannot read properties of null (reading 'filter') which became an unhandled rejection,
crashing the process.

Root cause: When a Duplex.fromWeb() stream is corked, writes
are batched into a _writev call. If destroy() is called in the
same microtask (after uncork()), the underlying WritableStream
writer gets aborted, causing writer.ready to reject. The done
callback then receives a single error instead of the array it
expects from SafePromiseAll.

Fix: Check whether error is an array before calling
ArrayPrototypeFilter on it. This handles both code paths:

  • SafePromiseAll resolution/rejection → array of results/errors
  • writer.ready rejection → single error value

Also uses ArrayIsArray/ArrayPrototypeFilter from primordials,
consistent with Node.js internal conventions.

Includes a regression test that reproduces the exact scenario from
the issue report (cork → write → write → uncork → destroy).

Fixes: #62199

The `done` callback in the `writev` handlers of both
`newStreamWritableFromWritableStream` and
`newStreamDuplexFromReadableWritablePair` unconditionally called
`error.filter()`, assuming `error` is always an array from
`SafePromiseAll`. However, `done` is also used as the rejection
handler for `writer.ready`, which passes a single error value,
not an array. This caused a `TypeError: error.filter is not a
function` which became an unhandled rejection, crashing the
process.

Fix by checking whether `error` is an array before filtering,
and also use `ArrayIsArray`/`ArrayPrototypeFilter` from primordials
to follow Node.js internal conventions.

Fixes: nodejs#62199
@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. web streams labels Mar 17, 2026
Comment on lines +785 to +787
// When error comes from SafePromiseAll, it is an array of
// errors (one per chunk). When it comes from writer.ready
// rejection, it is a single error. Normalize to handle both.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this assumption was correct originally? If an array is returned by Promise.all, then it means that all of the passed Promises resolved successfully. If any of them fail, then Promise.all will reject with the first rejection reason, not an array.

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

Labels

needs-ci PRs that need a full CI run. web streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplex.fromWeb leads to rare internal unhandled rejection

3 participants