diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 5a83849086294f..620ad46fce41a9 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -137,6 +137,7 @@ function OutgoingMessage(options) { this[kNeedDrain] = false; this.finished = false; + this._finishEmitted = false; this._headerSent = false; this[kCorked] = 0; this[kChunkedBuffer] = []; @@ -1032,6 +1033,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { function onFinish(outmsg) { if (outmsg?.socket?._hadError) return; + outmsg._finishEmitted = true; outmsg.emit('finish'); } @@ -1207,6 +1209,7 @@ function(err, event) { module.exports = { kHighWaterMark, + kErrored, kUniqueHeaders, parseUniqueHeadersOption, validateHeaderName, diff --git a/lib/_http_server.js b/lib/_http_server.js index 0b4077bf07367a..641930dc1cd670 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -52,6 +52,7 @@ const { } = require('_http_common'); const { ConnectionsList } = internalBinding('http_parser'); const { + kErrored, kUniqueHeaders, parseUniqueHeadersOption, OutgoingMessage, @@ -288,8 +289,16 @@ function onServerResponseClose() { // Ergo, we need to deal with stale 'close' events and handle the case // where the ServerResponse object has already been deconstructed. // Fortunately, that requires only a single if check. :-) - if (this._httpMessage) { - emitCloseNT(this._httpMessage); + const res = this._httpMessage; + if (res) { + if (res.finished && !res._finishEmitted && !res.destroyed) { + const err = new ConnResetException('aborted'); + res[kErrored] = err; + if (res.listenerCount('error') > 0) { + res.emit('error', err); + } + } + emitCloseNT(res); } } diff --git a/test/parallel/test-http-server-response-close-error.js b/test/parallel/test-http-server-response-close-error.js new file mode 100644 index 00000000000000..750c46f1bb2806 --- /dev/null +++ b/test/parallel/test-http-server-response-close-error.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { once } = require('events'); +const http = require('http'); + +async function main() { + const seen = []; + let responseClose; + + const server = http.createServer(common.mustCall((req, res) => { + responseClose = new Promise((resolve) => res.once('close', resolve)); + + res.on('finish', common.mustNotCall()); + res.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ECONNRESET'); + assert.strictEqual(err.message, 'aborted'); + seen.push('error'); + })); + res.on('close', common.mustCall(() => { + seen.push('close'); + })); + + req.socket.destroy(); + setImmediate(() => res.end('unreachable')); + })); + + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + + const req = http.get(`http://127.0.0.1:${server.address().port}/`); + await once(req, 'error'); + await responseClose; + + server.close(); + await once(server, 'close'); + + assert.deepStrictEqual(seen, ['error', 'close']); +} + +main().then(common.mustCall()); diff --git a/test/parallel/test-http-writable-true-after-close.js b/test/parallel/test-http-writable-true-after-close.js index 5e9c2300c5b19b..c6926769557fc3 100644 --- a/test/parallel/test-http-writable-true-after-close.js +++ b/test/parallel/test-http-writable-true-after-close.js @@ -20,6 +20,10 @@ const server = createServer(common.mustCall((req, res) => { res.on('finish', listener); // Everywhere else, 'close' is emitted res.on('close', listener); + res.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ECONNRESET'); + assert.strictEqual(err.message, 'aborted'); + })); get(`http://127.0.0.1:${internal.address().port}`, common.mustCall((inner) => { inner.pipe(res); diff --git a/test/parallel/test-stream-pipeline.js b/test/parallel/test-stream-pipeline.js index 8852f24a75050f..9269cb14c1b8b2 100644 --- a/test/parallel/test-stream-pipeline.js +++ b/test/parallel/test-stream-pipeline.js @@ -270,7 +270,10 @@ tmpdir.refresh(); { const server = http.createServer(common.mustCallAtLeast((req, res) => { - pipeline(req, res, common.mustSucceed()); + pipeline(req, res, common.mustCall((err) => { + assert.strictEqual(err.code, 'ECONNRESET'); + assert.strictEqual(err.message, 'aborted'); + })); })); server.listen(0, common.mustCall(() => {