blob: cc754477b788c3d9426824081c7113273b226999 [file] [edit]
'use strict';
// Regression test for a keep-alive socket reuse race condition.
//
// The race is between responseOnEnd() and requestOnFinish(), both of which
// can call responseKeepAlive(). The window is: req.end() has been called,
// the socket write has completed (writableFinished true), but the write
// callback that emits the 'finish' event has not fired yet.
//
// HTTPS widens this window because the TLS layer introduces async
// indirection between the actual write completion and the JS callback.
//
// With Expect: 100-continue, the server responds quickly while the client
// delays req.end() just slightly (setTimeout 0), creating the perfect
// timing for the response to arrive in that window.
//
// On unpatched Node, the double responseKeepAlive() call corrupts the
// socket by stripping a subsequent request's listeners and emitting a
// spurious 'free' event, causing requests to hang / time out.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const https = require('https');
const fixtures = require('../common/fixtures');
const REQUEST_COUNT = 100;
const agent = new https.Agent({ keepAlive: true, maxSockets: 1 });
const key = fixtures.readKey('agent1-key.pem');
const cert = fixtures.readKey('agent1-cert.pem');
const server = https.createServer({ key, cert }, common.mustCall((req, res) => {
req.on('error', common.mustNotCall());
res.writeHead(200);
res.end();
}, REQUEST_COUNT));
server.listen(0, common.mustCall(() => {
const { port } = server.address();
async function run() {
try {
for (let i = 0; i < REQUEST_COUNT; i++) {
await sendRequest(port);
}
} finally {
agent.destroy();
server.close();
}
}
run().then(common.mustCall());
}));
function sendRequest(port) {
let timeout;
const promise = new Promise((resolve, reject) => {
function done(err) {
clearTimeout(timeout);
if (err)
reject(err);
else
resolve();
}
const req = https.request({
port,
host: '127.0.0.1',
rejectUnauthorized: false,
method: 'POST',
agent,
headers: {
'Content-Length': '0',
'Expect': '100-continue',
},
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.once('end', done);
res.once('error', done);
}));
timeout = setTimeout(() => {
const err = new Error('request timed out');
req.destroy(err);
done(err);
}, common.platformTimeout(5000));
req.once('error', done);
setTimeout(() => req.end(Buffer.alloc(0)), 0);
});
return promise.finally(() => clearTimeout(timeout));
}