-
Notifications
You must be signed in to change notification settings - Fork 29.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
stream: prevent 'end' to be emitted after 'error' #20104
Conversation
CI: https://ci.nodejs.org/job/node-test-pull-request/14353/ cc @nodejs/streams @mafintosh |
cc @davepacheco |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM but want to give it another look before full on approval.
lib/_stream_readable.js
Outdated
@@ -99,6 +99,9 @@ function ReadableState(options, stream, isDuplex) { | |||
this.endEmitted = false; | |||
this.reading = false; | |||
|
|||
// flipped if an 'error' is emitted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: capitalize & end with a dot. (And yes, I know it totally doesn't match the rest of this file but it is our new style guide...)
@@ -424,12 +424,22 @@ function onwriteError(stream, state, sync, er, cb) { | |||
// this can emit finish, and it will always happen | |||
// after error | |||
process.nextTick(finishMaybe, stream, state); | |||
|
|||
// needed for duplex, fixes https://github.com/nodejs/node/issues/6083 | |||
if (stream._readableState) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't looked in enough detail but I guess there's no way to do this in _stream_duplex.js
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My goal would be to change this into stream.destroy(err)
. But that's a more massive change, see #20096.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can't we only use one of these and put it outside the wrapper if
instead of duplicating it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can't as the two branches are significantly different. However we can move it in its own function. Should I do that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm why, in this branch there are only two next tick calls so it's not a problem, in the else
below there is only cb(er);
before but I think it's not necessary to call the callback before setting the flag. I mean something like this:
diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js
index d21daf0541..04b344bc26 100644
--- a/lib/_stream_writable.js
+++ b/lib/_stream_writable.js
@@ -417,6 +417,13 @@ function doWrite(stream, state, writev, len, chunk, encoding, cb) {
function onwriteError(stream, state, sync, er, cb) {
--state.pendingcb;
+ // needed for duplex, fixes https://github.com/nodejs/node/issues/6083
+ if (stream._readableState) {
+ stream._readableState.errorEmitted = true;
+ }
+
+ stream._writableState.errorEmitted = true;
+
if (sync) {
// defer the callback if we are being called synchronously
// to avoid piling up things on the stack
@@ -424,13 +431,11 @@ function onwriteError(stream, state, sync, er, cb) {
// this can emit finish, and it will always happen
// after error
process.nextTick(finishMaybe, stream, state);
- stream._writableState.errorEmitted = true;
stream.emit('error', er);
} else {
// the caller expect this to happen before if
// it is async
cb(er);
- stream._writableState.errorEmitted = true;
stream.emit('error', er);
// this can emit finish, but finish must
// always follow error
but I didn't test it. If it doesn't work or if I am missing something, ignore me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the second block, you are setting errorEmitted
before calling the callback. I remember spending a significant amount of time on this block of code, and these two paths should remain separated. It's something I'd prefer not to refactor in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes stream._writableState.errorEmitted = true
can stay as is if the user provided callback needs to find it set to false
(why?) but I see no reason to not move the other part. There can't be existing code that relies on new code.
Anyway it's a just a nit and it can be ignored.
lib/internal/streams/destroy.js
Outdated
@@ -6,12 +6,15 @@ function destroy(err, cb) { | |||
this._readableState.destroyed; | |||
const writableDestroyed = this._writableState && | |||
this._writableState.destroyed; | |||
const readableErrored = this._readableState && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we create these inside the if
block just below? It doesn't look like they're used outside of it.
We should probably at least let this bake before LTS if not outright not land it. The fact that we even have tests that rely on this behaviour is concerning... |
This should definitely bake for a bit before going into LTS. |
This PR adds _readableState.errorEmitted and add the tracking of it. Fixes: nodejs#6083
9b0fd56
to
5b69058
Compare
duplex.on('end', common.mustNotCall()); | ||
|
||
duplex.end('hello'); | ||
duplex.on('data', function() {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: duplex.resume()
is cleaner.
Ops I commented on the fork. |
lib/internal/streams/destroy.js
Outdated
if (cb) { | ||
cb(err); | ||
} else if (err && | ||
(!this._writableState || !this._writableState.errorEmitted)) { | ||
} else if (err && !(readableErrored || writableErrored)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: maybe it's just me but I find it a lot easier to read if De Morgan is applied.
@@ -21,6 +21,7 @@ if (process.argv[2] === 'child') { | |||
assert.strictEqual(signal, null); | |||
})); | |||
|
|||
child.stderr.pipe(process.stderr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
CITGM (master): https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/1372/ |
CI (only the ones that failed): https://ci.nodejs.org/job/node-test-commit-linux/18088/ |
What is this semver wise? This should be semver-major, right? |
@BridgeAR yes, this is semver-major imho. |
This solves an extremely bad bug that has been open for a long period of time. The underlining contract of streams is that after This relies on the I'm certain that this could lend on 10 as a minor or patch, due to the new error semantics that @mafintosh has added in #19836. In fact, it should be in as a way to strengthen that invariant. As of backporting to 8, this is possible, as the current behavior is not deterministic (the test case in #6083 fails 50% of the time). @mafintosh, what do you think? |
Still LGTM |
LGTM from me. I'm noting that in my userland stream modules we already consider the first error to be "terminal", and ignore any following events because you can't rely on their behaivor after an error. Good job @mcollina This is a semver-minor, as it's a bug fix. I would only attempt a backport if it is easy to do so (you know the complexity of this more than me @mcollina), as the userland modules handles these cases right now either way. |
landing as a semver-minor. It would be fantastic if this could go into Node 10 @jasnell. Feel free to remove if from the milestone if it's over the finishing line. |
If it's a bug fix why semver-minor, it doesn't add anything that should be publicly accessible. |
removed semver-minor. |
Landed in 0857790 |
This PR adds _readableState.errorEmitted and add the tracking of it. Fixes: #6083 PR-URL: #20104 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]>
This PR adds _readableState.errorEmitted and add the tracking of it. Fixes: #6083 PR-URL: #20104 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]>
If we are to keep this change, I think this should be marked semver-major as it is a change in behavior and can break existing code (I stumbled upon this myself when trying to debug with node v10). There is not even any indication that I could find in the documentation that this is the intended behavior. Otherwise, we should revert this IMO because it didn't even have time to land in a v9 release and was merged only 5 days ago. /cc @nodejs/collaborators |
This PR adds _readableState.errorEmitted and add the tracking of it.
Fixes: #6083
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes