Skip to content

Commit 53b4700

Browse files
barathraj048aduh95
authored andcommitted
http: don't destroy socket after request completes
Aborting a ClientRequest after the request has finished sending and the response has fully arrived has nothing left to cancel. destroy() still called socket.destroy(err), but the resulting 'error' is emitted on a later tick. In that window, responseKeepAlive() has already removed socketErrorListener while handing the socket back to the agent's free pool, so the error lands with no listener and crashes the process. Skip the socket destroy when there is nothing left to cancel. This matches the existing behavior of keepAlive: false requests, which already drop any unread buffered response data in this situation. Fixes: #65938 Signed-off-by: Barath <barathraj048@gmail.com> PR-URL: #65952 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 67cb305 commit 53b4700

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

‎lib/_http_client.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,14 @@ ClientRequest.prototype.destroy = function destroy(err) {
691691
this.res._dump();
692692
}
693693

694+
// Nothing left to cancel: the request was fully sent and the response was
695+
// fully received. Destroying the socket here would emit an error on a
696+
// socket that is already being released to the agent, at which point
697+
// socketErrorListener has been removed and nothing would handle it.
698+
if (this.writableFinished && this.res?.complete) {
699+
return this;
700+
}
701+
694702
this[kError] = err;
695703
this.socket?.destroy(err);
696704

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
6+
// Aborting a request whose exchange has already completed must not destroy the
7+
// socket that is being released to the agent. socketErrorListener has been
8+
// removed by responseKeepAlive() at that point, so the error would be emitted
9+
// on a socket with no 'error' listener and crash the process.
10+
// Refs: https://github.com/nodejs/node/issues/65938
11+
12+
const agent = new http.Agent({ keepAlive: true });
13+
14+
const server = http.createServer((req, res) => {
15+
res.end('x');
16+
});
17+
18+
server.listen(0, '127.0.0.1', common.mustCall(() => {
19+
const controller = new AbortController();
20+
21+
const req = http.get({
22+
port: server.address().port,
23+
host: '127.0.0.1',
24+
agent,
25+
signal: controller.signal,
26+
}, common.mustCall(async (res) => {
27+
res.on('error', common.mustNotCall());
28+
29+
for await (const chunk of res) {
30+
assert.strictEqual(chunk.length, 1);
31+
assert.strictEqual(res.complete, true);
32+
assert.strictEqual(req.writableFinished, true);
33+
controller.abort(new Error('stop reading'));
34+
break;
35+
}
36+
37+
// The socket must survive the abort and go back to the pool, and a
38+
// subsequent request must be able to reuse it.
39+
const res2 = await new Promise((resolve, reject) => {
40+
const req2 = http.get({
41+
port: server.address().port,
42+
host: '127.0.0.1',
43+
agent,
44+
}, resolve);
45+
req2.on('error', reject);
46+
});
47+
48+
let body = '';
49+
for await (const chunk of res2) body += chunk;
50+
assert.strictEqual(body, 'x');
51+
52+
agent.destroy();
53+
server.close();
54+
}));
55+
56+
req.on('error', common.mustNotCall());
57+
}));

0 commit comments

Comments
 (0)