Commit 04bd7378a2a for nodejs

commit 04bd7378a2a07b996efc8c3c6ad0abb2e743c704
Author: James M Snell <jasnell@gmail.com>
Date:   Sun Oct 4 01:07:37 2026 +0000

    stream: add pipeToSync() failOnIncompleteClose option

    pipeToSync() does not fail the writer when endSync() returns -1, so
    that a caller can still close it asynchronously. A caller that cannot,
    e.g. because the writer is sync-only and has no end(), would be left
    with a writer that is neither closed nor failed.

    Add a failOnIncompleteClose option (a Node.js extension) that fails
    the writer with the thrown ERR_INVALID_STATE error in that case.
    preventFail takes precedence over it.

    Assisted-by: OpenCode
    Signed-off-by: James M Snell <jasnell@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66483
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>

diff --git a/doc/api/stream_iter.md b/doc/api/stream_iter.md
index ca584a8bc5f..49605b15c0d 100644
--- a/doc/api/stream_iter.md
+++ b/doc/api/stream_iter.md
@@ -716,6 +716,10 @@ added:
 * `...transforms` {Function|Object} Zero or more sync transforms.
 * `writer` {Object} Destination with `write(chunk)` method.
 * `options` {Object}
+  * `failOnIncompleteClose` {boolean} If `true`, call `writer.fail()` when
+    `writer.endSync()` cannot close the writer synchronously. Ignored when
+    `preventFail` is `true`. This option is a Node.js extension.
+    **Default:** `false`.
   * `preventClose` {boolean} **Default:** `false`.
   * `preventFail` {boolean} **Default:** `false`.
 * Returns: {number} Total bytes written.
@@ -730,8 +734,10 @@ The `writer` must have the `*Sync` methods (`writeSync`, `writevSync`,
 `writer.endSync()` returns `-1` because the writer cannot close synchronously
 (for example, a `push()` writer whose consumer has not read all of the data
 yet), `pipeToSync()` throws `ERR_INVALID_STATE`. All of the data was accepted
-by then, so the writer is not failed: it can still be closed, for example with
-`await writer.end()`.
+by then, so by default the writer is not failed: it can still be closed, for
+example with `await writer.end()`. If the writer cannot be closed any other
+way (for example, it has no `end()` method), or the caller will not close it,
+set `failOnIncompleteClose` to fail it with the thrown error instead.

 ### `pull(source[, ...transforms][, options])`

diff --git a/lib/internal/streams/iter/pull.js b/lib/internal/streams/iter/pull.js
index 1307c98bce6..176d8dbfc83 100644
--- a/lib/internal/streams/iter/pull.js
+++ b/lib/internal/streams/iter/pull.js
@@ -1088,11 +1088,16 @@ function pipeToSync(source, ...args) {

   // endSync() returning -1 only means that the writer cannot close
   // synchronously; every chunk was accepted. pipeToSync() never falls back to
-  // the async end(), so report it, but leave the writer as it is: the caller
-  // can still close it, e.g. with `await writer.end()`.
+  // the async end(), so report it. By default the writer is left as it is so
+  // that the caller can still close it (e.g. with `await writer.end()`);
+  // `failOnIncompleteClose` fails it instead, for callers that cannot.
   if (!closedSync) {
-    throw new ERR_INVALID_STATE(
+    const error = new ERR_INVALID_STATE(
       'Writer could not be closed synchronously');
+    if (options.failOnIncompleteClose && !options.preventFail) {
+      failWriterQuietly(writer, error);
+    }
+    throw error;
   }

   return totalBytes;
diff --git a/lib/internal/streams/iter/webidl.js b/lib/internal/streams/iter/webidl.js
index 273a52443bf..ef61abb7794 100644
--- a/lib/internal/streams/iter/webidl.js
+++ b/lib/internal/streams/iter/webidl.js
@@ -104,6 +104,13 @@ converters.PipeToOptions = createDictionaryConverter('PipeToOptions', [
 ]);
 converters.PipeToSyncOptions = createDictionaryConverter(
   'PipeToSyncOptions', [
+    // Node.js extension.
+    {
+      __proto__: null,
+      key: 'failOnIncompleteClose',
+      converter: baseConverters.boolean,
+      defaultValue: () => false,
+    },
     {
       __proto__: null,
       key: 'preventClose',
diff --git a/test/parallel/test-stream-iter-pipeto-edge.js b/test/parallel/test-stream-iter-pipeto-edge.js
index 56e8b027808..0c5448db5a8 100644
--- a/test/parallel/test-stream-iter-pipeto-edge.js
+++ b/test/parallel/test-stream-iter-pipeto-edge.js
@@ -129,9 +129,57 @@ async function testFailThrowingDoesNotMaskError() {
     (error) => error === signal.reason);
 }

+// failOnIncompleteClose fails a writer that cannot be closed synchronously,
+// e.g. a sync-only writer that has no end().
+async function testPipeToSyncFailOnIncompleteClose() {
+  let failReason;
+  const writer = {
+    writeSync() { return true; },
+    endSync: common.mustCall(() => -1),
+    fail: common.mustCall((reason) => { failReason = reason; }),
+  };
+  assert.throws(
+    () => pipeToSync(fromSync('data'), writer, { failOnIncompleteClose: true }),
+    (error) => {
+      assert.strictEqual(error.code, 'ERR_INVALID_STATE');
+      assert.strictEqual(error, failReason);
+      return true;
+    });
+
+  // preventFail takes precedence.
+  assert.throws(
+    () => pipeToSync(fromSync('data'), {
+      writeSync() { return true; },
+      endSync: common.mustCall(() => -1),
+      fail: common.mustNotCall(),
+    }, { failOnIncompleteClose: true, preventFail: true }),
+    { code: 'ERR_INVALID_STATE' });
+
+  // It has no effect when the writer closes synchronously.
+  assert.strictEqual(pipeToSync(fromSync('data'), {
+    writeSync() { return true; },
+    endSync: common.mustCall(() => 4),
+    fail: common.mustNotCall(),
+  }, { failOnIncompleteClose: true }), 4);
+
+  // A push() writer is failed with the error, so its consumer sees it.
+  const { writer: pushWriter, readable } = push();
+  let thrown;
+  assert.throws(() => {
+    try {
+      pipeToSync(fromSync('abc'), pushWriter, { failOnIncompleteClose: true });
+    } catch (error) {
+      thrown = error;
+      throw error;
+    }
+  }, { code: 'ERR_INVALID_STATE' });
+  await assert.rejects(text(readable), (error) => error === thrown);
+}
+
 Promise.all([
   testPipeToSyncEndSyncFailure(),
   testPipeToSyncEndSyncFailureDoesNotFailWriter(),
+  testPipeToSyncFailOnIncompleteClose(),
   testPipeToSyncNoEndSync(),
   testPipeToSyncPreventFail(),
   testPipeToSyncPreventClose(),