diff --git a/doc/api/errors.md b/doc/api/errors.md
index e809b26f6d0d9c..a812d74c8f26ea 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1703,6 +1703,12 @@ An attempt was made to call [`stream.pipe()`][] on a [`Writable`][] stream.
A stream method was called that cannot complete because the stream was
destroyed using `stream.destroy()`.
+
+### ERR_STREAM_ALREADY_FINISHED
+
+A stream method was called that cannot complete because the stream was
+finished.
+
### ERR_STREAM_NULL_VALUES
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
index d2c603bb301d09..32a51d120bad2b 100644
--- a/lib/_http_outgoing.js
+++ b/lib/_http_outgoing.js
@@ -46,6 +46,7 @@ const {
ERR_INVALID_CHAR,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_STREAM_CANNOT_PIPE,
+ ERR_STREAM_ALREADY_FINISHED,
ERR_STREAM_WRITE_AFTER_END
},
hideStackFrames
@@ -704,6 +705,13 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
}
if (this.finished) {
+ if (typeof callback === 'function') {
+ if (!this.writableFinished) {
+ this.on('finish', callback);
+ } else {
+ callback(new ERR_STREAM_ALREADY_FINISHED('end'));
+ }
+ }
return this;
}
diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js
index c6d4ec4266f4aa..9a01cb1c06cd4b 100644
--- a/lib/_stream_writable.js
+++ b/lib/_stream_writable.js
@@ -41,6 +41,7 @@ const {
ERR_MULTIPLE_CALLBACK,
ERR_STREAM_CANNOT_PIPE,
ERR_STREAM_DESTROYED,
+ ERR_STREAM_ALREADY_FINISHED,
ERR_STREAM_NULL_VALUES,
ERR_STREAM_WRITE_AFTER_END,
ERR_UNKNOWN_ENCODING
@@ -591,6 +592,13 @@ Writable.prototype.end = function(chunk, encoding, cb) {
// Ignore unnecessary end() calls.
if (!state.ending)
endWritable(this, state, cb);
+ else if (typeof cb === 'function') {
+ if (!state.finished) {
+ this.once('finish', cb);
+ } else {
+ cb(new ERR_STREAM_ALREADY_FINISHED('end'));
+ }
+ }
return this;
};
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index f8c42ff9b2777d..5c0d328a1b5eb3 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1128,6 +1128,9 @@ E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error);
E('ERR_SRI_PARSE',
'Subresource Integrity string %s had an unexpected at %d',
SyntaxError);
+E('ERR_STREAM_ALREADY_FINISHED',
+ 'Cannot call %s after a stream was finished',
+ Error);
E('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable', Error);
E('ERR_STREAM_DESTROYED', 'Cannot call %s after a stream was destroyed', Error);
E('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError);
diff --git a/test/parallel/test-http-outgoing-end-multiple.js b/test/parallel/test-http-outgoing-end-multiple.js
new file mode 100644
index 00000000000000..7c43e1f59d5849
--- /dev/null
+++ b/test/parallel/test-http-outgoing-end-multiple.js
@@ -0,0 +1,27 @@
+'use strict';
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+
+const server = http.createServer(common.mustCall(function(req, res) {
+ res.end('testing ended state', common.mustCall());
+ res.end(common.mustCall());
+ res.on('finish', common.mustCall(() => {
+ res.end(common.mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED');
+ server.close();
+ }));
+ }));
+}));
+
+server.listen(0);
+
+server.on('listening', common.mustCall(function() {
+ http
+ .request({
+ port: server.address().port,
+ method: 'GET',
+ path: '/'
+ })
+ .end();
+}));
diff --git a/test/parallel/test-stream-writable-end-multiple.js b/test/parallel/test-stream-writable-end-multiple.js
new file mode 100644
index 00000000000000..a94676ab8ab366
--- /dev/null
+++ b/test/parallel/test-stream-writable-end-multiple.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const common = require('../common');
+
+const assert = require('assert');
+const stream = require('stream');
+
+const writable = new stream.Writable();
+
+writable._write = (chunk, encoding, cb) => {
+ setTimeout(() => cb(), 10);
+};
+
+writable.end('testing ended state', common.mustCall());
+writable.end(common.mustCall());
+writable.on('finish', common.mustCall(() => {
+ writable.end(common.mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED');
+ }));
+}));