Skip to content

Commit 9e7f255

Browse files
committed
stream: pipeline should only destroy un-finished streams
This PR logically reverts nodejs#31940 which has caused lots of unnecessary breakage in the ecosystem. This PR also aligns better with the actual documented behavior: `stream.pipeline()` will call `stream.destroy(err)` on all streams except: * `Readable` streams which have emitted `'end'` or `'close'`. * `Writable` streams which have emitted `'finish'` or `'close'`. The behavior introduced in nodejs#31940 was much more aggressive in terms of destroying streams. This was good for avoiding potential resources leaks however breaks some common assumputions in legacy streams. Furthermore, it makes the code simpler and removes some hacks. Fixes: nodejs#32954 Fixes: nodejs#32955 PR-URL: nodejs#32968 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Mathias Buus <[email protected]> Backport-PR-URL: nodejs#32980
1 parent 947ddec commit 9e7f255

File tree

2 files changed

+149
-44
lines changed

2 files changed

+149
-44
lines changed

lib/internal/streams/pipeline.js

+11-40
Original file line numberDiff line numberDiff line change
@@ -25,52 +25,23 @@ let EE;
2525
let PassThrough;
2626
let createReadableStreamAsyncIterator;
2727

28-
function isIncoming(stream) {
29-
return (
30-
stream.socket &&
31-
typeof stream.complete === 'boolean' &&
32-
ArrayIsArray(stream.rawTrailers) &&
33-
ArrayIsArray(stream.rawHeaders)
34-
);
35-
}
36-
37-
function isOutgoing(stream) {
38-
return (
39-
stream.socket &&
40-
typeof stream.setHeader === 'function'
41-
);
42-
}
43-
44-
function destroyer(stream, reading, writing, final, callback) {
28+
function destroyer(stream, reading, writing, callback) {
4529
callback = once(callback);
46-
let destroyed = false;
30+
31+
let finished = false;
32+
stream.on('close', () => {
33+
finished = true;
34+
});
4735

4836
if (eos === undefined) eos = require('internal/streams/end-of-stream');
4937
eos(stream, { readable: reading, writable: writing }, (err) => {
50-
if (destroyed) return;
51-
destroyed = true;
52-
53-
if (!err && (isIncoming(stream) || isOutgoing(stream))) {
54-
// http/1 request objects have a coupling to their response and should
55-
// not be prematurely destroyed. Assume they will handle their own
56-
// lifecycle.
57-
return callback();
58-
}
59-
60-
if (!err && reading && !writing && stream.writable) {
61-
return callback();
62-
}
63-
64-
if (err || !final || !stream.readable) {
65-
destroyImpl.destroyer(stream, err);
66-
}
67-
38+
finished = !err;
6839
callback(err);
6940
});
7041

7142
return (err) => {
72-
if (destroyed) return;
73-
destroyed = true;
43+
if (finished) return;
44+
finished = true;
7445
destroyImpl.destroyer(stream, err);
7546
callback(err || new ERR_STREAM_DESTROYED('pipe'));
7647
};
@@ -192,7 +163,7 @@ function pipeline(...streams) {
192163

193164
if (isStream(stream)) {
194165
finishCount++;
195-
destroys.push(destroyer(stream, reading, writing, !reading, finish));
166+
destroys.push(destroyer(stream, reading, writing, finish));
196167
}
197168

198169
if (i === 0) {
@@ -250,7 +221,7 @@ function pipeline(...streams) {
250221
ret = pt;
251222

252223
finishCount++;
253-
destroys.push(destroyer(ret, false, true, true, finish));
224+
destroys.push(destroyer(ret, false, true, finish));
254225
}
255226
} else if (isStream(stream)) {
256227
if (isReadable(ret)) {

test/parallel/test-stream-pipeline.js

+138-4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ const {
77
Readable,
88
Transform,
99
pipeline,
10-
PassThrough
10+
PassThrough,
11+
Duplex
1112
} = require('stream');
1213
const assert = require('assert');
1314
const http = require('http');
1415
const { promisify } = require('util');
16+
const net = require('net');
1517

1618
{
1719
let finished = false;
@@ -917,7 +919,7 @@ const { promisify } = require('util');
917919
const src = new PassThrough({ autoDestroy: false });
918920
const dst = new PassThrough({ autoDestroy: false });
919921
pipeline(src, dst, common.mustCall(() => {
920-
assert.strictEqual(src.destroyed, true);
922+
assert.strictEqual(src.destroyed, false);
921923
assert.strictEqual(dst.destroyed, false);
922924
}));
923925
src.end();
@@ -959,8 +961,8 @@ const { promisify } = require('util');
959961
}
960962

961963
{
962-
const src = new PassThrough();
963-
const dst = new PassThrough();
964+
const src = new PassThrough({ autoDestroy: true });
965+
const dst = new PassThrough({ autoDestroy: true });
964966
dst.readable = false;
965967
pipeline(src, dst, common.mustCall((err) => {
966968
assert(!err);
@@ -1061,3 +1063,135 @@ const { promisify } = require('util');
10611063
assert.ifError(err);
10621064
}));
10631065
}
1066+
1067+
{
1068+
let closed = false;
1069+
const src = new Readable({
1070+
read() {},
1071+
destroy(err, cb) {
1072+
process.nextTick(cb);
1073+
}
1074+
});
1075+
const dst = new Writable({
1076+
write(chunk, encoding, callback) {
1077+
callback();
1078+
}
1079+
});
1080+
src.on('close', () => {
1081+
closed = true;
1082+
});
1083+
src.push(null);
1084+
pipeline(src, dst, common.mustCall((err) => {
1085+
assert.strictEqual(closed, true);
1086+
}));
1087+
}
1088+
1089+
{
1090+
let closed = false;
1091+
const src = new Readable({
1092+
read() {},
1093+
destroy(err, cb) {
1094+
process.nextTick(cb);
1095+
}
1096+
});
1097+
const dst = new Duplex({});
1098+
src.on('close', common.mustCall(() => {
1099+
closed = true;
1100+
}));
1101+
src.push(null);
1102+
pipeline(src, dst, common.mustCall((err) => {
1103+
assert.strictEqual(closed, true);
1104+
}));
1105+
}
1106+
1107+
{
1108+
const server = net.createServer(common.mustCall((socket) => {
1109+
// echo server
1110+
pipeline(socket, socket, common.mustCall());
1111+
// 13 force destroys the socket before it has a chance to emit finish
1112+
socket.on('finish', common.mustCall(() => {
1113+
server.close();
1114+
}));
1115+
})).listen(0, common.mustCall(() => {
1116+
const socket = net.connect(server.address().port);
1117+
socket.end();
1118+
}));
1119+
}
1120+
1121+
{
1122+
const d = new Duplex({
1123+
autoDestroy: false,
1124+
write: common.mustCall((data, enc, cb) => {
1125+
d.push(data);
1126+
cb();
1127+
}),
1128+
read: common.mustCall(() => {
1129+
d.push(null);
1130+
}),
1131+
final: common.mustCall((cb) => {
1132+
setTimeout(() => {
1133+
assert.strictEqual(d.destroyed, false);
1134+
cb();
1135+
}, 1000);
1136+
}),
1137+
destroy: common.mustNotCall()
1138+
});
1139+
1140+
const sink = new Writable({
1141+
write: common.mustCall((data, enc, cb) => {
1142+
cb();
1143+
})
1144+
});
1145+
1146+
pipeline(d, sink, common.mustCall());
1147+
1148+
d.write('test');
1149+
d.end();
1150+
}
1151+
1152+
{
1153+
const server = net.createServer(common.mustCall((socket) => {
1154+
// echo server
1155+
pipeline(socket, socket, common.mustCall());
1156+
socket.on('finish', common.mustCall(() => {
1157+
server.close();
1158+
}));
1159+
})).listen(0, common.mustCall(() => {
1160+
const socket = net.connect(server.address().port);
1161+
socket.end();
1162+
}));
1163+
}
1164+
1165+
{
1166+
const d = new Duplex({
1167+
autoDestroy: false,
1168+
write: common.mustCall((data, enc, cb) => {
1169+
d.push(data);
1170+
cb();
1171+
}),
1172+
read: common.mustCall(() => {
1173+
d.push(null);
1174+
}),
1175+
final: common.mustCall((cb) => {
1176+
setTimeout(() => {
1177+
assert.strictEqual(d.destroyed, false);
1178+
cb();
1179+
}, 1000);
1180+
}),
1181+
// `destroy()` won't be invoked by pipeline since
1182+
// the writable side has not completed when
1183+
// the pipeline has completed.
1184+
destroy: common.mustNotCall()
1185+
});
1186+
1187+
const sink = new Writable({
1188+
write: common.mustCall((data, enc, cb) => {
1189+
cb();
1190+
})
1191+
});
1192+
1193+
pipeline(d, sink, common.mustCall());
1194+
1195+
d.write('test');
1196+
d.end();
1197+
}

0 commit comments

Comments
 (0)