From f4379f5d0142f20c2e2e2887d6ab7418838321ea Mon Sep 17 00:00:00 2001 From: SapirBaruch Date: Mon, 18 May 2026 19:44:56 +0300 Subject: [PATCH 1/3] fix: replace deprecated 'aborted' event with 'close' on IncomingMessage --- lib/request.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/request.js b/lib/request.js index 2c40cdc98..4b05c8a38 100755 --- a/lib/request.js +++ b/lib/request.js @@ -325,7 +325,7 @@ exports = module.exports = internals.Request = class { this.raw.res.on('close', internals.event.bind(this.raw.res, this._eventContext, 'close')); this.raw.req.on('error', internals.event.bind(this.raw.req, this._eventContext, 'error')); - this.raw.req.on('aborted', internals.event.bind(this.raw.req, this._eventContext, 'abort')); + this.raw.req.on('close', internals.aborted.bind(this.raw.req, this._eventContext, this.raw.res)); this.raw.res.once('close', internals.closed.bind(this.raw.res, this)); } @@ -713,6 +713,19 @@ internals.closed = function (request) { request._closed = true; }; + +internals.aborted = function (eventContext, res) { + + // The 'aborted' event on IncomingMessage was deprecated in Node.js v17 and removed in v24. + // Use the 'close' event on the request socket instead, guarded by a check that the response + // has not already been written (to distinguish a client abort from a normal connection close). + + if (!res.writableEnded) { + internals.event(eventContext, 'abort'); + } +}; + + internals.event = function ({ request }, event, err) { if (!request) { From 96da532a95057fc64c05dd5422816ed6f011e379 Mon Sep 17 00:00:00 2001 From: SapirBaruch Date: Tue, 19 May 2026 12:10:18 +0300 Subject: [PATCH 2/3] test: add regression test for close-event disconnect detection Adds a test verifying that request.active() returns false when the client closes the connection before a response is sent. The test uses Net.connect so it works regardless of hostname resolution, and polls active() directly without relying on the 'disconnect' event. Covers the Node.js v24+ path where IncomingMessage 'aborted' is gone. --- test/request.js | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/request.js b/test/request.js index d2f1760e8..de68ad856 100755 --- a/test/request.js +++ b/test/request.js @@ -474,6 +474,56 @@ describe('Request', () => { testComplete: false }); }); + + it('returns false after client closes connection before response is sent', { retry: true }, async (flags) => { + + // Regression test: IncomingMessage 'aborted' event was removed in Node.js v24. + // Verify that an early client disconnect still causes active() to return false. + + const handlerTeam = new Teamwork.Team(); + + const server = Hapi.server(); + flags.onCleanup = () => server.stop(); + + let client; + + server.route({ + method: 'GET', + path: '/', + options: { + handler: async (request) => { + + // Drop the connection from the client side + client.destroy(); + + // Poll until the server-side close propagates + const deadline = Date.now() + 2000; + while (request.active() && Date.now() < deadline) { + await Hoek.wait(10); + } + + handlerTeam.attend({ active: request.active() }); + return null; + } + } + }); + + await server.start(); + + await new Promise((resolve) => { + + client = Net.connect(server.info.port, () => { + + client.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'); + resolve(); + }); + + client.on('error', Hoek.ignore); + }); + + const result = await handlerTeam.work; + expect(result.active).to.be.false(); + }); }); describe('_execute()', () => { From 97b5795206542f0322e5e7232e58ef09a29720eb Mon Sep 17 00:00:00 2001 From: SapirBaruch Date: Wed, 20 May 2026 14:47:50 +0300 Subject: [PATCH 3/3] fix: detect client disconnect on Node.js v24+ via res close event The 'aborted' event on IncomingMessage was deprecated in Node.js v17 and removed in Node.js v24. Restore compatibility by listening to both 'aborted' (for older Node) and the 'close' event on the response (for Node v24+ where 'aborted' no longer fires). A 'close' on the response before writableEnded indicates the client disconnected mid-request and is treated as an abort. @hapi/shot inject requests are excluded via the req._shot sentinel so that simulate.close scenarios are not misclassified as client aborts. --- lib/request.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/request.js b/lib/request.js index 4b05c8a38..a45899d3e 100755 --- a/lib/request.js +++ b/lib/request.js @@ -325,7 +325,13 @@ exports = module.exports = internals.Request = class { this.raw.res.on('close', internals.event.bind(this.raw.res, this._eventContext, 'close')); this.raw.req.on('error', internals.event.bind(this.raw.req, this._eventContext, 'error')); - this.raw.req.on('close', internals.aborted.bind(this.raw.req, this._eventContext, this.raw.res)); + + // 'aborted' was deprecated in Node.js v17 and removed in v24. It remains here for + // compatibility with older Node.js versions where it is the most reliable abort signal. + // For Node.js v24+, where 'aborted' no longer fires, the 'close' event on the response + // (handled in internals.event) serves as the fallback abort indicator. + this.raw.req.on('aborted', internals.event.bind(this.raw.req, this._eventContext, 'abort')); + this.raw.res.once('close', internals.closed.bind(this.raw.res, this)); } @@ -714,17 +720,6 @@ internals.closed = function (request) { }; -internals.aborted = function (eventContext, res) { - - // The 'aborted' event on IncomingMessage was deprecated in Node.js v17 and removed in v24. - // Use the 'close' event on the request socket instead, guarded by a check that the response - // has not already been written (to distinguish a client abort from a normal connection close). - - if (!res.writableEnded) { - internals.event(eventContext, 'abort'); - } -}; - internals.event = function ({ request }, event, err) { @@ -752,7 +747,12 @@ internals.event = function ({ request }, event, err) { request._eventContext.request = null; - if (event === 'abort') { + // On Node.js v24+, 'aborted' was removed. A 'close' on the response before writableEnded + // means the client disconnected mid-request — treat it as an abort. We exclude @hapi/shot + // inject requests (identified by req._shot) because shot fires req.emit('close') for its + // simulate.close scenario but that should not be treated as a client abort. + if (event === 'abort' || + (event === 'close' && !request.raw.res.writableEnded && !request.raw.req._shot)) { // Calling _reply() means that the abort is applied immediately, unless the response has already // called _reply(), in which case this call is ignored and the transmit logic is responsible for