nodejs/test/parallel/test-http2-graceful-close.js
Kushagra Pandey 609df89cd3 http2: session tracking and graceful server close
This change adds proper tracking of HTTP / 2 server sessions
to ensure they are gracefully closed when the server is
shut down.It implements:

- A new kSessions symbol for tracking active sessions
- Adding/removing sessions from a SafeSet in the server
- A closeAllSessions helper function to close active sessions
- Updates to Http2Server and Http2SecureServer close methods

Breaking Change: any client trying to create new requests
on existing connections will not be able to do so once
server close is initiated

Refs: https://datatracker.ietf.org/doc/html/rfc7540\#section-9.1
Refs: https://nodejs.org/api/http.html\#serverclosecallback

- improve HTTP/2 server shutdown to prevent race conditions

1. Fix server shutdown race condition
   - Stop listening for new connections before closing existing ones
   - Ensure server.close() properly completes in all scenarios

2. Improve HTTP/2 tests
   - Replace setTimeout with event-based flow control
   - Simplify test logic for better readability
   - Add clear state tracking for event ordering
   - Improve assertions to verify correct shutdown sequence

This eliminates a race condition where new sessions could connect
between the time existing sessions are closed and the server stops
listening, potentially preventing the server from fully shutting down.

- fix cross-platform test timing issues

Fix test-http2-server-http1-client.js failure on Ubuntu
by deferring server.close() to next event loop cycle.

The issue only affected Ubuntu where session close occurs
before error emission, causing the test to miss errors
when HTTP/1 clients connect to HTTP/2 servers.

Using setImmediate() ensures error events fire before
server close across all platforms while maintaining
recent session handling improvements.

PR-URL: https://github.com/nodejs/node/pull/57586
Fixes: https://github.com/nodejs/node/issues/57611
Refs: https://datatracker.ietf.org/doc/html/rfc7540#section-9.1
Refs: https://nodejs.org/api/http.html#serverclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2025-04-19 10:17:12 -07:00

68 lines
1.8 KiB
JavaScript

'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
// This test verifies that the server waits for active connections/sessions
// to complete and all data to be sent before fully closing
// Create a server that will send a large response in chunks
const server = http2.createServer();
// Track server events
server.on('stream', common.mustCall((stream, headers) => {
// Initiate the server close before client data is sent, this will
// test if the server properly waits for the stream to finish
server.close();
setImmediate(() => {
stream.respond({
'content-type': 'text/plain',
':status': 200
});
// Create a large response (1MB) to ensure it takes some time to send
const chunk = Buffer.alloc(64 * 1024, 'x');
// Send 16 chunks (1MB total) to simulate a large response
for (let i = 0; i < 16; i++) {
stream.write(chunk);
}
// Stream end should happen after data is written
stream.end();
});
stream.on('close', common.mustCall(() => {
assert.strictEqual(stream.readableEnded, true);
assert.strictEqual(stream.writableFinished, true);
}));
}));
// Start the server
server.listen(0, common.mustCall(() => {
// Create client and request
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request({ ':path': '/' });
// Track received data
let receivedData = 0;
req.on('data', (chunk) => {
receivedData += chunk.length;
});
// When request closes
req.on('close', common.mustCall(() => {
// Should receive all data
assert.strictEqual(req.readableEnded, true);
assert.strictEqual(receivedData, 64 * 1024 * 16);
assert.strictEqual(req.writableFinished, true);
}));
// Start the request
req.end();
}));