current commit
260213.7 · http: fix keep-alive socket reuse race in requestOnFinish
'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));
}