Skip to content

Commit fa1239e

Browse files
committed
stream: eof & pipeline compat
Some broken streams might emit 'close' before 'end' or 'finish'. However, if they have actually been ended or finished, this should not result in a premature close error. Fixes: #29699
1 parent ba74fd8 commit fa1239e

File tree

6 files changed

+91
-14
lines changed

6 files changed

+91
-14
lines changed

doc/api/stream.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,11 @@ Especially useful in error handling scenarios where a stream is destroyed
15251525
prematurely (like an aborted HTTP request), and will not emit `'end'`
15261526
or `'finish'`.
15271527

1528+
`stream.finished()` will error with `ERR_STREAM_PREMATURE_CLOSE` if:
1529+
* `Writable` emits `'close'` before `'finish'` and
1530+
[`writable.writableFinished`][].
1531+
* `Readable` emits `'close'` before `'end'` and [`readable.readableEnded`][].
1532+
15281533
The `finished` API is promisify-able as well;
15291534

15301535
```js
@@ -1647,6 +1652,10 @@ run().catch(console.error);
16471652
* `Readable` streams which have emitted `'end'` or `'close'`.
16481653
* `Writable` streams which have emitted `'finish'` or `'close'`.
16491654

1655+
If any `Writable` or `Readable` stream emits `'close'` without being able to
1656+
fully flush or drain, `stream.pipeline()` will error with
1657+
`ERR_STREAM_PREMATURE_CLOSE`.
1658+
16501659
`stream.pipeline()` leaves dangling event listeners on the streams
16511660
after the `callback` has been invoked. In the case of reuse of streams after
16521661
failure, this can cause event listener leaks and swallowed errors.
@@ -2865,6 +2874,7 @@ contain multi-byte characters.
28652874
[`process.stdout`]: process.html#process_process_stdout
28662875
[`readable._read()`]: #stream_readable_read_size_1
28672876
[`readable.push('')`]: #stream_readable_push
2877+
[`readable.readableEnded`]: #stream_readable_readableended
28682878
[`readable.setEncoding()`]: #stream_readable_setencoding_encoding
28692879
[`stream.Readable.from()`]: #stream_stream_readable_from_iterable_options
28702880
[`stream.cork()`]: #stream_writable_cork

lib/internal/streams/end-of-stream.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,24 @@ function eos(stream, opts, callback) {
7979
};
8080

8181
const onclose = () => {
82-
if (readable && !readableEnded) {
83-
callback.call(stream, new ERR_STREAM_PREMATURE_CLOSE());
84-
} else if (writable && !writableFinished) {
85-
callback.call(stream, new ERR_STREAM_PREMATURE_CLOSE());
82+
if (readable) {
83+
const ended = (stream._readableState &&
84+
stream._readableState.endEmitted) || stream.readableEnded;
85+
if (!ended && !readableEnded) {
86+
callback.call(stream, new ERR_STREAM_PREMATURE_CLOSE());
87+
} else if (!readableEnded) {
88+
// Compat. Stream has ended but haven't emitted 'end'.
89+
callback.call(stream);
90+
}
91+
} else if (writable) {
92+
const finished = (stream._writableState &&
93+
stream._writableState.finished) || stream.writableFinished;
94+
if (!finished && !writableFinished) {
95+
callback.call(stream, new ERR_STREAM_PREMATURE_CLOSE());
96+
} else if (!writableFinished) {
97+
// Compat. Stream has finished but haven't emitted 'finish'.
98+
callback.call(stream);
99+
}
86100
}
87101
};
88102

lib/internal/streams/pipeline.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,29 @@ function destroyer(stream, reading, writing, callback) {
4646

4747
if (eos === undefined) eos = require('internal/streams/end-of-stream');
4848
eos(stream, { readable: reading, writable: writing }, (err) => {
49+
if (
50+
err &&
51+
err.code === 'ERR_STREAM_PREMATURE_CLOSE' &&
52+
reading &&
53+
(stream._readableState && stream._readableState.ended)
54+
) {
55+
// Some readable streams will emit 'close' before 'end'. However, since
56+
// this is on the readable side 'end' should still be emitted if the
57+
// stream has been ended and no error emitted. This should be allowed in
58+
// favor of backwards compatibility. Since the stream is piped to a
59+
// destination this should not result in any observable difference.
60+
// We don't need to check if this is a writable premature close since
61+
// eos will only fail with premature close on the reading side for
62+
// duplex streams.
63+
stream
64+
.on('end', () => {
65+
closed = true;
66+
callback();
67+
})
68+
.on('error', callback);
69+
return;
70+
}
71+
4972
if (err) return callback(err);
5073
closed = true;
5174
callback();

test/parallel/test-http-client-finished.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const { finished } = require('stream');
5555
}).end();
5656
finished(req, (err) => {
5757
common.expectsError({
58-
type: Error,
58+
name: 'Error',
5959
code: 'ERR_STREAM_PREMATURE_CLOSE'
6060
})(err);
6161
finished(req, common.mustCall(() => {

test/parallel/test-stream-finished.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,27 +273,26 @@ const { promisify } = require('util');
273273
}
274274

275275
{
276-
// Premature close if stream never emitted 'finish'
277-
// even if writableFinished says something else.
276+
// No error if stream never emitted 'end'
277+
// even if readableEnded says something else.
278278

279279
const streamLike = new EE();
280280
streamLike.writable = true;
281-
finished(streamLike, common.expectsError({
282-
code: 'ERR_STREAM_PREMATURE_CLOSE'
281+
finished(streamLike, common.mustCall((err) => {
282+
assert.strictEqual(err, undefined);
283283
}));
284284
streamLike.writableFinished = true;
285285
streamLike.emit('close');
286286
}
287287

288-
289288
{
290-
// Premature close if stream never emitted 'end'
291-
// even if readableEnded says something else.
289+
// No error if stream never emitted 'finished'
290+
// even if writableFinished says something else.
292291

293292
const streamLike = new EE();
294293
streamLike.readable = true;
295-
finished(streamLike, common.expectsError({
296-
code: 'ERR_STREAM_PREMATURE_CLOSE'
294+
finished(streamLike, common.mustCall((err) => {
295+
assert.strictEqual(err, undefined);
297296
}));
298297
streamLike.readableEnded = true;
299298
streamLike.emit('close');

test/parallel/test-stream-pipeline.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,4 +912,35 @@ const { promisify } = require('util');
912912
}, common.mustCall((err) => {
913913
assert.strictEqual(err.message, 'kaboom');
914914
}));
915+
// Make sure 'close' before 'end' finishes without error
916+
// if readable has received eof.
917+
// Ref: https://github.com/nodejs/node/issues/29699
918+
const r = new Readable();
919+
const w = new Writable({
920+
write(chunk, encoding, cb) {
921+
cb();
922+
}
923+
});
924+
pipeline(r, w, (err) => {
925+
assert.strictEqual(err, undefined);
926+
});
927+
r.push(null);
928+
r.destroy();
929+
}
930+
931+
{
932+
// Make sure 'close' before 'end' finishes without error
933+
// if readable has received eof.
934+
// Ref: https://github.com/nodejs/node/issues/29699
935+
const r = new Readable();
936+
const w = new Writable({
937+
write(chunk, encoding, cb) {
938+
cb();
939+
}
940+
});
941+
pipeline(r, w, (err) => {
942+
assert.strictEqual(err, undefined);
943+
});
944+
r.push(null);
945+
r.emit('close');
915946
}

0 commit comments

Comments
 (0)