Skip to content

Commit 6cecf14

Browse files
jasnellrichardlau
authored andcommitted
stream: do not fail the writer when pipeToSync() cannot close
After writing every chunk, pipeToSync() treated endSync() returning -1 like any other error: it threw ERR_INVALID_STATE and, unless preventFail was set, called writer.fail() with it. -1 only means that the writer cannot close synchronously, e.g. a push() writer whose consumer has not drained it yet. All of the data had been accepted, but failing the writer discarded it, so the consumer saw an error instead of the end of the stream, and the caller could not recover. pipeToSync() still throws ERR_INVALID_STATE in that case, since it never falls back to the async end(), but it no longer fails the writer. The caller can still close it, e.g. with `await writer.end()`. Assisted-by: OpenCode Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #66483 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 0e1c99a commit 6cecf14

3 files changed

Lines changed: 45 additions & 5 deletions

File tree

‎doc/api/stream_iter.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,13 @@ Synchronous version of [`pipeTo()`][]. The `source`, all transforms, and the
718718
The `writer` must have the `*Sync` methods (`writeSync`, `writevSync`,
719719
`endSync`) and `fail()` for this to work.
720720

721+
`pipeToSync()` never falls back to the asynchronous writer methods. If
722+
`writer.endSync()` returns `-1` because the writer cannot close synchronously
723+
(for example, a `push()` writer whose consumer has not read all of the data
724+
yet), `pipeToSync()` throws `ERR_INVALID_STATE`. All of the data was accepted
725+
by then, so the writer is not failed: it can still be closed, for example with
726+
`await writer.end()`.
727+
721728
### `pull(source[, ...transforms][, options])`
722729

723730
<!-- YAML

‎lib/internal/streams/iter/pull.js‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,7 @@ function pipeToSync(source, ...args) {
10481048
normalized;
10491049

10501050
let totalBytes = 0;
1051+
let closedSync = true;
10511052

10521053
try {
10531054
for (const batch of pipeline) {
@@ -1076,10 +1077,7 @@ function pipeToSync(source, ...args) {
10761077
}
10771078

10781079
if (!options.preventClose) {
1079-
if (FunctionPrototypeCall(endSync, writer) < 0) {
1080-
throw new ERR_INVALID_STATE(
1081-
'Writer could not be closed synchronously');
1082-
}
1080+
closedSync = FunctionPrototypeCall(endSync, writer) >= 0;
10831081
}
10841082
} catch (error) {
10851083
if (!options.preventFail) {
@@ -1088,6 +1086,15 @@ function pipeToSync(source, ...args) {
10881086
throw error;
10891087
}
10901088

1089+
// endSync() returning -1 only means that the writer cannot close
1090+
// synchronously; every chunk was accepted. pipeToSync() never falls back to
1091+
// the async end(), so report it, but leave the writer as it is: the caller
1092+
// can still close it, e.g. with `await writer.end()`.
1093+
if (!closedSync) {
1094+
throw new ERR_INVALID_STATE(
1095+
'Writer could not be closed synchronously');
1096+
}
1097+
10911098
return totalBytes;
10921099
}
10931100

‎test/parallel/test-stream-iter-pipeto-edge.js‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
const common = require('../common');
77
const assert = require('assert');
8-
const { pipeTo, pipeToSync, fromSync } = require('stream/iter');
8+
const {
9+
pipeTo, pipeToSync, fromSync, push, text,
10+
} = require('stream/iter');
911

1012
// pipeToSync cannot complete when endSync() requires async fallback.
1113
async function testPipeToSyncEndSyncFailure() {
@@ -22,6 +24,29 @@ async function testPipeToSyncEndSyncFailure() {
2224
assert.strictEqual(endCalled, false);
2325
}
2426

27+
// The data was accepted, so endSync() returning -1 does not fail the writer,
28+
// even without preventFail, and the caller can still close it.
29+
async function testPipeToSyncEndSyncFailureDoesNotFailWriter() {
30+
const writer = {
31+
writeSync() { return true; },
32+
endSync: common.mustCall(() => -1),
33+
end: common.mustNotCall(),
34+
fail: common.mustNotCall(),
35+
};
36+
assert.throws(() => pipeToSync(fromSync('data'), writer),
37+
{ code: 'ERR_INVALID_STATE' });
38+
39+
// A push() writer whose consumer has not read yet cannot close
40+
// synchronously. After the throw, the data is intact and the writer can be
41+
// ended asynchronously.
42+
const { writer: pushWriter, readable } = push();
43+
assert.throws(() => pipeToSync(fromSync(['abc', 'def']), pushWriter),
44+
{ code: 'ERR_INVALID_STATE' });
45+
const result = text(readable);
46+
assert.strictEqual(await pushWriter.end(), 6);
47+
assert.strictEqual(await result, 'abcdef');
48+
}
49+
2550
// pipeToSync requires endSync() when closing is enabled.
2651
async function testPipeToSyncNoEndSync() {
2752
let writeCalled = false;
@@ -106,6 +131,7 @@ async function testFailThrowingDoesNotMaskError() {
106131

107132
Promise.all([
108133
testPipeToSyncEndSyncFailure(),
134+
testPipeToSyncEndSyncFailureDoesNotFailWriter(),
109135
testPipeToSyncNoEndSync(),
110136
testPipeToSyncPreventFail(),
111137
testPipeToSyncPreventClose(),

0 commit comments

Comments
 (0)