stream: prevent 'end' to be emitted after 'error'

This PR adds _readableState.errorEmitted and add the tracking of it.

Fixes: https://github.com/nodejs/node/issues/6083

PR-URL: https://github.com/nodejs/node/pull/20104
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
This commit is contained in:
Matteo Collina 2018-04-17 17:24:22 +02:00
parent 700344e388
commit 0857790656
17 changed files with 76 additions and 20 deletions

View File

@ -99,6 +99,9 @@ function ReadableState(options, stream, isDuplex) {
this.endEmitted = false; this.endEmitted = false;
this.reading = false; this.reading = false;
// Flipped if an 'error' is emitted.
this.errorEmitted = false;
// a flag to be able to tell if the event 'readable'/'data' is emitted // a flag to be able to tell if the event 'readable'/'data' is emitted
// immediately, or on a later tick. We set this to true at first, because // immediately, or on a later tick. We set this to true at first, because
// any actions that shouldn't happen until "later" should generally also // any actions that shouldn't happen until "later" should generally also
@ -1069,20 +1072,23 @@ function fromList(n, state) {
function endReadable(stream) { function endReadable(stream) {
var state = stream._readableState; var state = stream._readableState;
debug('endReadable', state.endEmitted); debug('endReadable', state.endEmitted, state.errorEmitted);
if (!state.endEmitted) { if (!state.endEmitted && !state.errorEmitted) {
state.ended = true; state.ended = true;
process.nextTick(endReadableNT, state, stream); process.nextTick(endReadableNT, state, stream);
} }
} }
function endReadableNT(state, stream) { function endReadableNT(state, stream) {
debug('endReadableNT', state.endEmitted, state.length); debug('endReadableNT', state.endEmitted, state.length, state.errorEmitted);
// Check that we didn't get one last unshift. // Check that we didn't get one last unshift.
if (!state.endEmitted && state.length === 0) { if (!state.endEmitted && state.length === 0) {
state.endEmitted = true;
stream.readable = false; stream.readable = false;
stream.emit('end');
if (!state.errorEmitted) {
state.endEmitted = true;
stream.emit('end');
}
} }
} }

View File

@ -424,12 +424,22 @@ function onwriteError(stream, state, sync, er, cb) {
// this can emit finish, and it will always happen // this can emit finish, and it will always happen
// after error // after error
process.nextTick(finishMaybe, stream, state); process.nextTick(finishMaybe, stream, state);
// needed for duplex, fixes https://github.com/nodejs/node/issues/6083
if (stream._readableState) {
stream._readableState.errorEmitted = true;
}
stream._writableState.errorEmitted = true; stream._writableState.errorEmitted = true;
stream.emit('error', er); stream.emit('error', er);
} else { } else {
// the caller expect this to happen before if // the caller expect this to happen before if
// it is async // it is async
cb(er); cb(er);
// needed for duplex, fixes https://github.com/nodejs/node/issues/6083
if (stream._readableState) {
stream._readableState.errorEmitted = true;
}
stream._writableState.errorEmitted = true; stream._writableState.errorEmitted = true;
stream.emit('error', er); stream.emit('error', er);
// this can emit finish, but finish must // this can emit finish, but finish must

View File

@ -8,10 +8,14 @@ function destroy(err, cb) {
this._writableState.destroyed; this._writableState.destroyed;
if (readableDestroyed || writableDestroyed) { if (readableDestroyed || writableDestroyed) {
const readableErrored = this._readableState &&
this._readableState.errorEmitted;
const writableErrored = this._writableState &&
this._writableState.errorEmitted;
if (cb) { if (cb) {
cb(err); cb(err);
} else if (err && } else if (err && !readableErrored && !writableErrored) {
(!this._writableState || !this._writableState.errorEmitted)) {
process.nextTick(emitErrorNT, this, err); process.nextTick(emitErrorNT, this, err);
} }
return this; return this;
@ -32,6 +36,11 @@ function destroy(err, cb) {
this._destroy(err || null, (err) => { this._destroy(err || null, (err) => {
if (!cb && err) { if (!cb && err) {
process.nextTick(emitErrorAndCloseNT, this, err); process.nextTick(emitErrorAndCloseNT, this, err);
if (this._readableState) {
this._readableState.errorEmitted = true;
}
if (this._writableState) { if (this._writableState) {
this._writableState.errorEmitted = true; this._writableState.errorEmitted = true;
} }
@ -65,6 +74,7 @@ function undestroy() {
this._readableState.reading = false; this._readableState.reading = false;
this._readableState.ended = false; this._readableState.ended = false;
this._readableState.endEmitted = false; this._readableState.endEmitted = false;
this._readableState.errorEmitted = false;
} }
if (this._writableState) { if (this._writableState) {

View File

@ -95,7 +95,6 @@ const Countdown = require('../common/countdown');
}); });
req.resume(); req.resume();
req.on('end', common.mustCall());
req.on('close', common.mustCall(() => server.close())); req.on('close', common.mustCall(() => server.close()));
})); }));
} }

View File

@ -101,7 +101,6 @@ function runTest(test) {
}); });
} }
req.on('end', common.mustCall());
req.on('close', common.mustCall(() => { req.on('close', common.mustCall(() => {
client.destroy(); client.destroy();

View File

@ -45,5 +45,4 @@ server.listen(0, common.mustCall(() => {
req.on('response', common.mustNotCall()); req.on('response', common.mustNotCall());
req.resume(); req.resume();
req.on('end', common.mustCall());
})); }));

View File

@ -63,7 +63,6 @@ server.listen(0, common.mustCall(() => {
req.on('close', common.mustCall(() => countdown.dec())); req.on('close', common.mustCall(() => countdown.dec()));
req.resume(); req.resume();
req.on('end', common.mustCall());
} }
{ {
@ -78,6 +77,5 @@ server.listen(0, common.mustCall(() => {
req.on('close', common.mustCall(() => countdown.dec())); req.on('close', common.mustCall(() => countdown.dec()));
req.resume(); req.resume();
req.on('end', common.mustCall());
} }
})); }));

View File

@ -45,7 +45,6 @@ server.listen(0, common.mustCall(() => {
req.on('aborted', common.mustCall()); req.on('aborted', common.mustCall());
req.on('response', common.mustNotCall()); req.on('response', common.mustNotCall());
req.resume(); req.resume();
req.on('end', common.mustCall());
req.on('close', common.mustCall(() => countdown.dec())); req.on('close', common.mustCall(() => countdown.dec()));
req.on('error', common.expectsError({ req.on('error', common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR', code: 'ERR_HTTP2_STREAM_ERROR',

View File

@ -41,7 +41,6 @@ server.listen(0, common.mustCall(() => {
req.on('response', common.mustCall()); req.on('response', common.mustCall());
req.resume(); req.resume();
req.on('end', common.mustCall());
req.on('close', common.mustCall(() => { req.on('close', common.mustCall(() => {
server.close(); server.close();
client.close(); client.close();

View File

@ -53,7 +53,6 @@ server.listen(0, common.mustCall(() => {
// header to be set for non-payload bearing requests... // header to be set for non-payload bearing requests...
const req = client.request({ 'content-length': 1 }); const req = client.request({ 'content-length': 1 });
req.resume(); req.resume();
req.on('end', common.mustCall());
req.on('close', common.mustCall(() => countdown.dec())); req.on('close', common.mustCall(() => countdown.dec()));
req.on('error', common.expectsError({ req.on('error', common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR', code: 'ERR_HTTP2_STREAM_ERROR',

View File

@ -40,7 +40,7 @@ server.listen(0, () => {
req.on('response', common.mustCall()); req.on('response', common.mustCall());
req.on('error', common.mustCall(errorCheck)); req.on('error', common.mustCall(errorCheck));
req.on('data', common.mustNotCall()); req.on('data', common.mustNotCall());
req.on('end', common.mustCall(() => { req.on('close', common.mustCall(() => {
assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR);
client.close(); client.close();
server.close(); server.close();

View File

@ -87,7 +87,7 @@ function runTest(test) {
req.resume(); req.resume();
req.end(); req.end();
req.on('end', common.mustCall(() => { req.on('close', common.mustCall(() => {
client.close(); client.close();
if (!tests.length) { if (!tests.length) {

View File

@ -95,7 +95,7 @@ function runTest(test) {
req.resume(); req.resume();
req.end(); req.end();
req.on('end', common.mustCall(() => { req.on('close', common.mustCall(() => {
client.close(); client.close();
if (!tests.length) { if (!tests.length) {

View File

@ -32,5 +32,5 @@ server.on('listening', common.mustCall(() => {
})); }));
req.resume(); req.resume();
req.on('data', common.mustNotCall()); req.on('data', common.mustNotCall());
req.on('end', common.mustCall(() => server.close())); req.on('close', common.mustCall(() => server.close()));
})); }));

View File

@ -52,5 +52,4 @@ server.on('listening', common.mustCall(() => {
req.on('aborted', common.mustCall()); req.on('aborted', common.mustCall());
req.resume(); req.resume();
req.on('end', common.mustCall());
})); }));

View File

@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
const { Duplex } = require('stream');
const { strictEqual } = require('assert');
const duplex = new Duplex({
write(chunk, enc, cb) {
cb(new Error('kaboom'));
},
read() {
this.push(null);
}
});
duplex.on('error', common.mustCall(function() {
strictEqual(this._readableState.errorEmitted, true);
strictEqual(this._writableState.errorEmitted, true);
}));
duplex.on('end', common.mustNotCall());
duplex.end('hello');
duplex.resume();

View File

@ -189,3 +189,18 @@ const { inherits } = require('util');
read.push('hi'); read.push('hi');
read.on('data', common.mustNotCall()); read.on('data', common.mustNotCall());
} }
{
// double error case
const read = new Readable({
read() {}
});
read.on('close', common.mustCall());
read.on('error', common.mustCall());
read.destroy(new Error('kaboom 1'));
read.destroy(new Error('kaboom 2'));
assert.strictEqual(read._readableState.errorEmitted, true);
assert.strictEqual(read.destroyed, true);
}