◐ Shell
clean mode source ↗

http: fix leaked error listener on sync HTTP req create + destroy · nodejs/node@64f15c2

Original file line numberDiff line numberDiff line change

@@ -954,7 +954,8 @@ changes:

954954
955955

Destroy the request. Optionally emit an `'error'` event,

956956

and emit a `'close'` event. Calling this will cause remaining data

957-

in the response to be dropped and the socket to be destroyed.

957+

in the response to be dropped, and the socket to be destroyed if used,

958+

or returned to the corresponding Agent pool otherwise if possible.

958959
959960

See [`writable.destroy()`][] for further details.

960961
Original file line numberDiff line numberDiff line change

@@ -999,6 +999,7 @@ function onSocketNT(req, socket, err) {

999999

if (socket) {

10001000

if (!err && req.agent && !socket.destroyed) {

10011001

socket.emit('free');

1002+

socket.removeListener('error', socketErrorListener);

10021003

} else {

10031004

finished(socket.destroy(err || req[kError]), (er) => {

10041005

if (er?.code === 'ERR_STREAM_PREMATURE_CLOSE') {

Original file line numberDiff line numberDiff line change

@@ -0,0 +1,50 @@

1+

'use strict';

2+

const common = require('../common');

3+

const assert = require('assert');

4+

const http = require('http');

5+

const { defaultMaxListeners } = require('events');

6+
7+

// Immediately destroying an HTTP request must not leak listeners on the

8+

// freed socket. When sockets are reused via a keep-alive agent leaked

9+

// listeners would accumulate to trigger a MaxListenersExceededWarning.

10+
11+

// Check we don't get a MaxListenersExceededWarning:

12+

process.on('warning', common.mustNotCall('Unexpected warning emitted'));

13+
14+

const server = http.createServer(common.mustNotCall());

15+
16+

server.listen(0, common.mustCall(() => {

17+

const agent = new http.Agent({ keepAlive: true });

18+

const port = server.address().port;

19+
20+

// Count actual socket creations to confirm reuse:

21+

let createSocketCount = 0;

22+

const origCreateSocket = agent.createSocket.bind(agent);

23+

agent.createSocket = function(...args) {

24+

createSocketCount++;

25+

return origCreateSocket(...args);

26+

};

27+
28+

function executeHttpGet() {

29+

return new Promise((resolve) => {

30+

const req = http.get({ host: '127.0.0.1', port, agent });

31+

req.on('error', resolve);

32+

req.on('close', resolve);

33+

req.on('response', common.mustNotCall());

34+

req.destroy();

35+

});

36+

}

37+
38+

async function main() {

39+

for (let i = 0; i < defaultMaxListeners + 1; i++) {

40+

await executeHttpGet();

41+

}

42+
43+

assert.strictEqual(createSocketCount, 1);

44+
45+

server.close();

46+

agent.destroy();

47+

}

48+
49+

main().then(common.mustCall());

50+

}));