Skip to content
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

The 'error' event can be emitted more than once when using writable.destroy() #26015

Closed
lpinca opened this issue Feb 9, 2019 · 5 comments
Closed
Labels
stream Issues and PRs related to the stream subsystem.

Comments

@lpinca
Copy link
Member

lpinca commented Feb 9, 2019

  • Version: v11.9.0
  • Platform: macOS
  • Subsystem: stream

The 'error' event can be emitted multiple times when using writable.destroy() if the _destroy() callback is called asynchronously. Here is a test case:

const { Writable } = require('stream');

const writable = new Writable({
  destroy(err, callback) {
    process.nextTick(callback, new Error('oops'));
  }
});

writable.on('error', console.error);

writable.destroy();

// Assume an internal resource is closed in the `_destroy()` implementation.
// The resource fails to be closed cleanly causing `writable.destroy()` to be
// called again with an error.
writable.destroy(new Error('error'));

Actual result:

The 'error' event is emitted twice.

Expected result:

The 'error' event is emitted only once.

This is because the _writableState.errorEmitted guard is set to true when the callback is called.

@lpinca lpinca added the stream Issues and PRs related to the stream subsystem. label Feb 9, 2019
@lpinca
Copy link
Member Author

lpinca commented Feb 9, 2019

This seems to fix the issue without breaking any existing tests

diff --git a/lib/internal/streams/destroy.js b/lib/internal/streams/destroy.js
index 0c652be9dd..200c75459a 100644
--- a/lib/internal/streams/destroy.js
+++ b/lib/internal/streams/destroy.js
@@ -10,10 +10,15 @@ function destroy(err, cb) {
   if (readableDestroyed || writableDestroyed) {
     if (cb) {
       cb(err);
-    } else if (err &&
-               (!this._writableState || !this._writableState.errorEmitted)) {
-      process.nextTick(emitErrorNT, this, err);
+    } else if (err) {
+      if (!this._writableState) {
+        process.nextTick(emitErrorNT, this, err);
+      } else if (!this._writableState.errorEmitted) {
+        this._writableState.errorEmitted = true;
+        process.nextTick(emitErrorNT, this, err);
+      }
     }
+
     return this;
   }
 
@@ -31,9 +36,13 @@ function destroy(err, cb) {
 
   this._destroy(err || null, (err) => {
     if (!cb && err) {
-      process.nextTick(emitErrorAndCloseNT, this, err);
-      if (this._writableState) {
+      if (!this._writableState) {
+        process.nextTick(emitErrorAndCloseNT, this, err);
+      } else if (!this._writableState.errorEmitted) {
         this._writableState.errorEmitted = true;
+        process.nextTick(emitErrorAndCloseNT, this, err);
+      } else {
+        process.nextTick(emitCloseNT, this);
       }
     } else if (cb) {
       process.nextTick(emitCloseNT, this);

but is there a reason for having no guard at all for readable only streams?

@lpinca
Copy link
Member Author

lpinca commented Feb 12, 2019

cc: @nodejs/streams

@mcollina
Copy link
Member

but is there a reason for having no guard at all for readable only streams?

The time needed for making it happen and dealing with the potential ecosystem breakage. If you got some stretch of time, send a PR!

@mcollina
Copy link
Member

@lpinca can you PR the changes in #26015 (comment)?

@lpinca
Copy link
Member Author

lpinca commented Feb 12, 2019

Yes, will do.

lpinca added a commit to lpinca/node that referenced this issue Feb 12, 2019
Prevent the `'error'` event from being emitted multiple times if
`writable.destroy()` is called with an error before the `_destroy()`
callback is called.

Emit the first error, discard all others.

Fixes: nodejs#26015
BridgeAR pushed a commit to BridgeAR/node that referenced this issue Mar 12, 2019
Prevent the `'error'` event from being emitted multiple times if
`writable.destroy()` is called with an error before the `_destroy()`
callback is called.

Emit the first error, discard all others.

PR-URL: nodejs#26057
Fixes: nodejs#26015
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
BethGriggs pushed a commit that referenced this issue Apr 16, 2019
Prevent the `'error'` event from being emitted multiple times if
`writable.destroy()` is called with an error before the `_destroy()`
callback is called.

Emit the first error, discard all others.

PR-URL: #26057
Fixes: #26015
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
lpinca added a commit to lpinca/node that referenced this issue Jun 1, 2019
Prevent the `'error'` event from being emitted multiple times if
`writable.destroy()` is called with an error before the `_destroy()`
callback is called.

Emit the first error, discard all others.

PR-URL: nodejs#26057
Backport-PR-URL: nodejs#28000
Fixes: nodejs#26015
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
BethGriggs pushed a commit that referenced this issue Sep 19, 2019
Prevent the `'error'` event from being emitted multiple times if
`writable.destroy()` is called with an error before the `_destroy()`
callback is called.

Emit the first error, discard all others.

PR-URL: #26057
Backport-PR-URL: #28000
Fixes: #26015
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants