Skip to content
Closed
Prev Previous commit
Next Next commit
fix: 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.
  • Loading branch information
pandeykushagra51 committed Mar 29, 2025
commit 7a852db48479d691305ce3be3641a50af4437634
4 changes: 2 additions & 2 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -3224,11 +3224,11 @@ class Http2SecureServer extends TLSServer {
}

close() {
ReflectApply(TLSServer.prototype.close, this, arguments);
if (this[kOptions].allowHTTP1 === true) {
httpServerPreClose(this);
}
closeAllSessions(this);
ReflectApply(TLSServer.prototype.close, this, arguments);
}

closeIdleConnections() {
Expand Down Expand Up @@ -3266,8 +3266,8 @@ class Http2Server extends NETServer {
}

close() {
closeAllSessions(this);
ReflectApply(NETServer.prototype.close, this, arguments);
Comment thread
pimterry marked this conversation as resolved.
closeAllSessions(this);
}

async [SymbolAsyncDispose]() {
Expand Down
103 changes: 103 additions & 0 deletions test/parallel/test-http2-graceful-close.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'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

// Keep track of the test flow with these flags
const states = {
serverListening: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed,server.listen(0, common.mustCall()) already checks this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

responseStarted: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is not used/checked. I think it is not needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

dataFullySent: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably better to use stream.writableFinished instead. Setting this to true as soon as stream.end() is called does not guarantee that the data is actually written.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

streamClosed: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, stream.on('close', common.mustCall()) already checks this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

serverClosed: false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed. If the server does not close, the process does not exit and the test times out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all your feedback 😊 , I will be updating them soon

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

};

// 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) => {
assert.strictEqual(states.serverListening, true);

// Setup the response
stream.respond({
'content-type': 'text/plain',
':status': 200
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this after server.close, and move both this & the chunk writing into a setImmediate block below, to make sure that they run after server.close() here has done its thing.

I think it will still work, it's just a small change to cover the bases on what's considered 'idle' here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally make sense, will update these soon

Copy link
Copy Markdown
Contributor Author

@pandeykushagra51 pandeykushagra51 Apr 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done the suggested changes, please review once available


// Initiate the server close before client data is sent, this will
// test if the server properly waits for the stream to finish
server.close(common.mustCall(() => {
// Stream should be closed before server close callback
// Should be called only after the stream has closed
assert.strictEqual(states.streamClosed, true);
states.serverClosed = true;
}));

// Mark response as started
states.responseStarted = true;

// 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();
states.dataFullySent = true;

// When stream closes, we can verify order of events
stream.on('close', common.mustCall(() => {
// Data should be fully sent before stream closes
assert.strictEqual(states.dataFullySent, true);
states.streamClosed = true;
}));
}));

// Start the server
server.listen(0, common.mustCall(() => {
states.serverListening = true;

// 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 the request completes
req.on('end', common.mustCall(() => {
// All data should be sent before request ends
assert.strictEqual(states.dataFullySent, true);
}));

// When request closes
req.on('close', common.mustCall(() => {
// Check final state
assert.strictEqual(states.streamClosed, true);
// Should receive all data
assert.strictEqual(receivedData, 64 * 1024 * 16);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Should receive all data
assert.strictEqual(receivedData, 64 * 1024 * 16);
// Should receive all data
assert.strictEqual(req.readableEnded, true);
assert.strictEqual(receivedData, 64 * 1024 * 16);
assert.strictEqual(req.writableFinished, true);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Wait for the server close confirmation
process.on('exit', () => {
// Server should be fully closed
assert.strictEqual(states.serverClosed, true);
});
}));

// Start the request
req.end();
}));
217 changes: 0 additions & 217 deletions test/parallel/test-http2-request-after-server-close.js

This file was deleted.

Loading