From 0805ecb09f003930f2531fc6afd7956cf36a3509 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Tue, 19 May 2026 22:07:57 -0700 Subject: [PATCH 1/3] http: emit error on aborted ServerResponse Emit an error when a ServerResponse is closed after end() but before the finish event is emitted. Fixes: https://github.com/nodejs/node/issues/50656 Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 --- lib/_http_outgoing.js | 3 ++ lib/_http_server.js | 13 +++++- .../test-http-server-response-close-error.js | 42 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-http-server-response-close-error.js 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()); From 934e839f8e802859e58c4161a5fd33084fe6640a Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Tue, 19 May 2026 22:57:52 -0700 Subject: [PATCH 2/3] test: update stream pipeline abort expectation Expect ECONNRESET when an HTTP response pipeline is closed before the response finish event is emitted. --- test/parallel/test-stream-pipeline.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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(() => { From a098f1eab0dd75fe7c75b2b23ebc95e5a25e9421 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Tue, 19 May 2026 23:11:55 -0700 Subject: [PATCH 3/3] test: expect response abort errors Update HTTP tests that intentionally close the client connection to expect ECONNRESET from ServerResponse. --- test/parallel/test-http-writable-true-after-close.js | 4 ++++ 1 file changed, 4 insertions(+) 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);