const { once } = require('events'); const { promises: { readFile } } = require('fs'); const { connect, createSecureServer } = require('http2'); const path = require('path'); const { inspect } = require('util'); const PORT = 8443; const REQUEST_TIMEOUT = 10000; const TIME_BETWEEN_REQUESTS = { min: 0, max: 200 }; const REQUEST_PROCESSING_TIME = { min: 10, max: 100 }; const NUM_PARALLEL_SESSIONS = 25; const NUM_PARALLEL_REQUEST_LOOPS = 25; async function reproduce() { const server = await startServer(); try { await Promise.all(repeat(NUM_PARALLEL_SESSIONS, requestSessionLoop)); } catch (err) { console.error(err.stack); } finally { server.close(); } } async function startServer() { const [cert, key] = await Promise.all([ readFile(path.resolve(__dirname, './certs/bugrepro.org.crt')), readFile(path.resolve(__dirname, './certs/bugrepro.org.key')) ]); const server = createSecureServer({ cert, key }, handleRequest); server .on('session', session => { //console.log('Started new session', session); }) .on('sessionError', err => { console.error('Got session error:', err.stack); }) server.listen(PORT); await once(server, 'listening'); return server; } async function handleRequest(req, res) { await delay(randomRange(REQUEST_PROCESSING_TIME)); const contentLength = parseInt(req.headers['content-length'], 10); const buffers = []; let bytesRead = 0; let lastChunkState; req .on('data', chunk => { bytesRead += chunk.length; buffers.push(chunk); if (bytesRead === contentLength) { lastChunkState = inspect(req); } }) .once('end', () => { try { const concatenated = buffers.join(''); const body = JSON.parse(concatenated); //console.log('Got request:', JSON.stringify(body)); res.statusCode = 204; res.end(); } catch (err) { res.statusCode = 500; res.end(err.stack); } }) .once('aborted', () => { console.log(`Server received abort event from client. Bytes received: ${bytesRead} out of ${contentLength}.`); console.log('\nRequest state after reading last chunk:'); console.log(lastChunkState); console.log('\nRequest state after abort:') console.dir(req); }) .once('error', err => { console.error('Got error from request stream'); res.statusCode = 500; res.end(err.stack); }); } let sessionCounter = 0; let requestCounter = 0; async function requestSessionLoop() { const ca = await readFile(path.resolve(__dirname, './certs/fakeca.crt')); const session = connect(`https://bugrepro.org:${PORT}`, { ca }); const sessionId = ++sessionCounter; session .on('error', err => { console.error(`Received error from H2 session: ${err.stack}`); }) .on('frameError', err => { console.error(`Received frame error from H2 session: ${err.stack}`); }) .on('timeout', () => { console.log('Session timed out'); }) await once(session, 'connect'); await Promise.all(repeat(NUM_PARALLEL_REQUEST_LOOPS, () => sendPostsInLoop(session, sessionId))); } async function sendPostsInLoop(session, sessionId) { while (true) { const stream = session.request({ ':method': 'POST', ':path': '/', 'content-type': 'application/json' }); stream.setTimeout(REQUEST_TIMEOUT); stream.end(JSON.stringify({ sessionId, request: ++requestCounter, random: Math.random(), some: { arbitrary: 'content'.repeat(500) } })); await verifyResponse(stream); await delay(randomRange(TIME_BETWEEN_REQUESTS)); } } async function verifyResponse(req) { return new Promise((resolve, reject) => { req .once('error', err => { reject(new Error(`Received error from request stream: ${err.stack}`)); }) .once('frameError', err => { reject(new Error(`Received frame error from request stream: ${err.stack}`)); }) .once('aborted', () => { reject(new Error(`Request was aborted`)); }) .once('timeout', () => { reject(new Error(`Request timed out`)); }) .once('response', (headers, flags) => { const responseBuffer = []; req .on('data', chunk => { responseBuffer.push(chunk); }) .once('end', () => { const body = responseBuffer.join(''); const status = headers[':status']; if (status >= 400) { reject(new Error(`Received unexpected HTTP error ${status} with body: ${body}`)); } else { resolve(); } }) }); }); } function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function repeat(times, fn) { const result = []; for (let i = 0; i < times; i++) { result.push(fn()); } return result; } function randomRange({ min, max }) { const magnitude = max - min; return Math.random() * magnitude + min; } reproduce().catch(console.error);