Skip to content

Commit 39af61f

Browse files
addaleaxBethGriggs
authored andcommitted
stream: fix end-of-stream for HTTP/2
HTTP/2 streams call `.end()` on themselves from their `.destroy()` method, which might be queued (e.g. due to network congestion) and not processed before the stream itself is destroyed. In that case, the `_writableState.ended` property could be set before the stream emits its `'close'` event, and never actually emits the `'finished'` event, confusing the end-of-stream implementation so that it wouldn’t call its callback. This can be fixed by watching for the end events themselves using the existing `'finish'` and `'end'` listeners rather than relying on the `.ended` properties of the `_...State` objects. These properties still need to be checked to know whether stream closure was premature – My understanding is that ideally, streams should not emit `'close'` before `'end'` and/or `'finished'`, so this might be another bug, but changing this would require modifying tests and almost certainly be a breaking change. Fixes: #24456 PR-URL: #24926 Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
1 parent b2e6cbd commit 39af61f

3 files changed

Lines changed: 53 additions & 9 deletions

File tree

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,24 @@ function eos(stream, opts, callback) {
3636

3737
callback=once(callback);
3838

39-
constws=stream._writableState;
40-
constrs=stream._readableState;
4139
letreadable=opts.readable||(opts.readable!==false&&stream.readable);
4240
letwritable=opts.writable||(opts.writable!==false&&stream.writable);
4341

4442
constonlegacyfinish=()=>{
4543
if(!stream.writable)onfinish();
4644
};
4745

46+
varwritableEnded=stream._writableState&&stream._writableState.finished;
4847
constonfinish=()=>{
4948
writable=false;
49+
writableEnded=true;
5050
if(!readable)callback.call(stream);
5151
};
5252

53+
varreadableEnded=stream._readableState&&stream._readableState.endEmitted;
5354
constonend=()=>{
5455
readable=false;
56+
readableEnded=true;
5557
if(!writable)callback.call(stream);
5658
};
5759

@@ -60,11 +62,16 @@ function eos(stream, opts, callback) {
6062
};
6163

6264
constonclose=()=>{
63-
if(readable&&!(rs&&rs.ended)){
64-
returncallback.call(stream,newERR_STREAM_PREMATURE_CLOSE());
65+
leterr;
66+
if(readable&&!readableEnded){
67+
if(!stream._readableState||!stream._readableState.ended)
68+
err=newERR_STREAM_PREMATURE_CLOSE();
69+
returncallback.call(stream,err);
6570
}
66-
if(writable&&!(ws&&ws.ended)){
67-
returncallback.call(stream,newERR_STREAM_PREMATURE_CLOSE());
71+
if(writable&&!writableEnded){
72+
if(!stream._writableState||!stream._writableState.ended)
73+
err=newERR_STREAM_PREMATURE_CLOSE();
74+
returncallback.call(stream,err);
6875
}
6976
};
7077

@@ -77,7 +84,7 @@ function eos(stream, opts, callback) {
7784
stream.on('abort',onclose);
7885
if(stream.req)onrequest();
7986
elsestream.on('request',onrequest);
80-
}elseif(writable&&!ws){// legacy streams
87+
}elseif(writable&&!stream._writableState){// legacy streams
8188
stream.on('end',onlegacyfinish);
8289
stream.on('close',onlegacyfinish);
8390
}

‎test/parallel/parallel.status‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ test-net-connect-options-port: PASS,FLAKY
1212
test-http2-pipe: PASS,FLAKY
1313
test-worker-syntax-error: PASS,FLAKY
1414
test-worker-syntax-error-file: PASS,FLAKY
15-
# https://github.com/nodejs/node/issues/24456
16-
test-stream-pipeline-http2: PASS,FLAKY
1715

1816
[$system==linux]
1917

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
constcommon=require('../common');
3+
constassert=require('assert');
4+
const{ Readable, Duplex, pipeline }=require('stream');
5+
6+
// Test that the callback for pipeline() is called even when the ._destroy()
7+
// method of the stream places an .end() request to itself that does not
8+
// get processed before the destruction of the stream (i.e. the 'close' event).
9+
// Refs: https://github.com/nodejs/node/issues/24456
10+
11+
constreadable=newReadable({
12+
read: common.mustCall(()=>{})
13+
});
14+
15+
constduplex=newDuplex({
16+
write(chunk,enc,cb){
17+
// Simulate messages queueing up.
18+
},
19+
read(){},
20+
destroy(err,cb){
21+
// Call end() from inside the destroy() method, like HTTP/2 streams
22+
// do at the time of writing.
23+
this.end();
24+
cb(err);
25+
}
26+
});
27+
28+
duplex.on('finished',common.mustNotCall());
29+
30+
pipeline(readable,duplex,common.mustCall((err)=>{
31+
assert.strictEqual(err.code,'ERR_STREAM_PREMATURE_CLOSE');
32+
}));
33+
34+
// Write one chunk of data, and destroy the stream later.
35+
// That should trigger the pipeline destruction.
36+
readable.push('foo');
37+
setImmediate(()=>{
38+
readable.destroy();
39+
});

0 commit comments

Comments
 (0)