Skip to content

Commit 5f312b3

Browse files
committed
http: adds regression test for issue #60001 against an HTTP server
1 parent 2ea056a commit 5f312b3

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
'use strict';
2+
3+
// Regression test for a keep-alive socket reuse race condition.
4+
//
5+
// The race is between responseOnEnd() and requestOnFinish(), both of which
6+
// can call responseKeepAlive(). The window is: req.end() has been called,
7+
// the socket write has completed (writableFinished true), but the write
8+
// callback that emits the 'finish' event has not fired yet.
9+
//
10+
// With plain HTTP the window is normally too narrow to hit. This test
11+
// widens it by delaying every client-socket write *callback* by a few
12+
// milliseconds (the actual I/O is not delayed, so writableFinished becomes
13+
// true while the 'finish'-emitting callback is still pending).
14+
//
15+
// With Expect: 100-continue, the server responds quickly while the client
16+
// delays req.end() just slightly (setTimeout 0), creating the perfect
17+
// timing for the response to arrive in that window.
18+
//
19+
// On unpatched Node, the double responseKeepAlive() call corrupts the
20+
// socket by stripping a subsequent request's listeners and emitting a
21+
// spurious 'free' event, causing requests to hang / time out.
22+
23+
const common = require('../common');
24+
const assert = require('assert');
25+
const http = require('http');
26+
const net = require('net');
27+
28+
const REQUEST_COUNT = 100;
29+
const agent = new http.Agent({ keepAlive: true, maxSockets: 1 });
30+
31+
// Delay every write *callback* on the client socket so that
32+
// socket.writableLength drops to 0 (writableFinished becomes true) before
33+
// the callback that ultimately emits the 'finish' event fires. With
34+
// HTTPS the TLS layer provides this gap naturally; for plain HTTP we
35+
// need to create it artificially.
36+
const patchedSockets = new WeakSet();
37+
function patchSocket(socket) {
38+
if (patchedSockets.has(socket)) return;
39+
patchedSockets.add(socket);
40+
const delay = 5;
41+
const origWrite = socket.write;
42+
socket.write = function(chunk, encoding, cb) {
43+
if (typeof encoding === 'function') {
44+
cb = encoding;
45+
encoding = null;
46+
}
47+
if (typeof cb === 'function') {
48+
const orig = cb;
49+
cb = (...args) => setTimeout(() => orig(...args), delay);
50+
}
51+
return origWrite.call(this, chunk, encoding, cb);
52+
};
53+
}
54+
55+
const server = http.createServer(common.mustCall((req, res) => {
56+
req.on('error', common.mustNotCall());
57+
res.writeHead(200);
58+
res.end();
59+
}, REQUEST_COUNT));
60+
61+
server.listen(0, common.mustCall(() => {
62+
const { port } = server.address();
63+
64+
async function run() {
65+
try {
66+
for (let i = 0; i < REQUEST_COUNT; i++) {
67+
await sendRequest(port);
68+
}
69+
} finally {
70+
agent.destroy();
71+
server.close();
72+
}
73+
}
74+
75+
run().then(common.mustCall());
76+
}));
77+
78+
function sendRequest(port) {
79+
let timeout;
80+
const promise = new Promise((resolve, reject) => {
81+
function done(err) {
82+
clearTimeout(timeout);
83+
if (err)
84+
reject(err);
85+
else
86+
resolve();
87+
}
88+
89+
const req = http.request({
90+
port,
91+
host: '127.0.0.1',
92+
method: 'POST',
93+
agent,
94+
headers: {
95+
'Content-Length': '0',
96+
Expect: '100-continue',
97+
},
98+
}, common.mustCall((res) => {
99+
assert.strictEqual(res.statusCode, 200);
100+
res.resume();
101+
res.once('end', done);
102+
res.once('error', done);
103+
}));
104+
105+
req.on('socket', patchSocket);
106+
107+
timeout = setTimeout(() => {
108+
const err = new Error('request timed out');
109+
req.destroy(err);
110+
done(err);
111+
}, common.platformTimeout(5000));
112+
113+
req.once('error', done);
114+
115+
setTimeout(() => req.end(Buffer.alloc(0)), 0);
116+
});
117+
return promise.finally(() => clearTimeout(timeout));
118+
}

0 commit comments

Comments
 (0)