| '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)); |
| } |