Skip to content

stream: fix writev unhandled rejection in fromWeb#62297

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

stream: fix writev unhandled rejection in fromWeb#62297
Han5991 wants to merge 1 commit intonodejs:mainfrom
Han5991:fix/webstreams-duplex-writev-unhandled-rejection

Conversation

@Han5991
Copy link
Copy Markdown
Contributor

@Han5991 Han5991 commented Mar 17, 2026

Summary

Fixes #62199

When using Duplex.fromWeb() or Writable.fromWeb() with cork()/uncork(), writes are batched into _writev(). If destroy() is called in the same microtask (after uncork()), the underlying WritableStream writer gets aborted, causing SafePromiseAll() to reject with a non-array value (e.g. an AbortError or null).

The done() callback in _writev() of both newStreamDuplexFromReadableWritablePair and newStreamWritableFromWritableStream unconditionally called error.filter(), assuming the value was always an array from SafePromiseAll. This caused a TypeError: Cannot read properties of null (reading 'filter') that became an unhandled rejection, crashing the process.

Root cause

done was used as both the resolve and reject handler for SafePromiseAll:

PromisePrototypeThen(SafePromiseAll(chunks, ...), done, done);
  • Resolve path: done([undefined, undefined, ...]) — array, .filter() works
  • Reject path: done(abortError) or done(null) — non-array, .filter() throws

Fix

Separate the resolve and reject handlers of SafePromiseAll. Promise.all resolving means all writes succeeded — there is no error to report. Promise.all rejecting passes a single error value directly.

PromisePrototypeThen(SafePromiseAll(chunks, ...), () => done(), done);
  • () => done() — resolve path: all writes succeeded, call callback with no error
  • done — reject path: single error passed directly to callback

The same fix is applied to both newStreamDuplexFromReadableWritablePair and newStreamWritableFromWritableStream.

Test

Added test/parallel/test-webstreams-duplex-fromweb-writev-unhandled-rejection.js with the exact reproduction from the issue report (cork → write → write → uncork → destroy).

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. web streams labels Mar 17, 2026
@Renegade334
Copy link
Copy Markdown
Member

#62291 is already open (although I believe the approach here is the correct one)

@Han5991
Copy link
Copy Markdown
Contributor Author

Han5991 commented Mar 17, 2026

Agreed — and since #62291 is already open, I've opened this as a separate issue because I believe the approach here is the correct one. @Renegade334

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.71%. Comparing base (bf1aebc) to head (5df0476).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #62297      +/-   ##
==========================================
- Coverage   89.72%   89.71%   -0.01%     
==========================================
  Files         695      695              
  Lines      214464   214462       -2     
  Branches    41067    41059       -8     
==========================================
- Hits       192420   192410      -10     
  Misses      14106    14106              
- Partials     7938     7946       +8     
Files with missing lines Coverage Δ
lib/internal/webstreams/adapters.js 86.60% <100.00%> (+0.15%) ⬆️

... and 28 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Han5991
Copy link
Copy Markdown
Contributor Author

Han5991 commented Apr 4, 2026

@Renegade334

Could you possibly review this PR once again? I am aware that there is a previous PR, but it seems quite some time has passed.

@Han5991 Han5991 force-pushed the fix/webstreams-duplex-writev-unhandled-rejection branch from 1af4a4a to 7ce5532 Compare April 4, 2026 10:11
When using Duplex.fromWeb() or Writable.fromWeb() with cork()/uncork(),
writes are batched into _writev(). If destroy() is called in the same
microtask, the underlying WritableStream writer gets aborted, causing
SafePromiseAll() to reject with a non-array value (e.g. an AbortError).

The done() callback in _writev() of both fromWeb adapter functions
unconditionally called error.filter(), assuming the value was always an
array. This caused a TypeError that became an unhandled rejection,
crashing the process.

Fix by separating the resolve and reject handlers of SafePromiseAll:
use () => done() on the resolve path (all writes succeeded, no error)
and done on the reject path (error passed directly to callback).

Fixes: nodejs#62199
Signed-off-by: sangwook <rewq5991@gmail.com>
@Han5991 Han5991 force-pushed the fix/webstreams-duplex-writev-unhandled-rejection branch from 7ce5532 to 5df0476 Compare April 4, 2026 10:13
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