Commit 663f666fdae for nodejs
commit 663f666fdaedb920d87371468324c909fee2cf2e
Author: Huang-404-Q <129407632+Huang-404-Q@users.noreply.github.com>
Date: Wed Oct 28 21:31:19 2026 +0800
http2: clear the session timeout before marking it destroyed
closeSession() set SESSION_FLAGS_DESTROYED and then called
session.setTimeout(0). setStreamTimeout() returns early when the
session is destroyed, so the idle timer stayed armed and retained
the session until the server timeout fired.
Clear the timeout and remove its listeners before setting the
destroyed flag.
Fixes: https://github.com/nodejs/node/issues/66885
Signed-off-by: huang <13165998033@163.com>
PR-URL: https://github.com/nodejs/node/pull/66886
Reviewed-By: Tim Perry <pimterry@gmail.com>
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 011cf22210f..04acb5d30f6 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -1303,13 +1303,16 @@ function closeSession(session, code, error) {
debugSessionObj(session, 'start closing/destroying', error);
const state = session[kState];
- state.flags |= SESSION_FLAGS_DESTROYED;
- state.destroyCode = code;
- // Clear timeout and remove timeout listeners.
+ // Clear timeout and remove timeout listeners before marking the session
+ // destroyed: setTimeout() is a no-op once destroyed, so the idle timer
+ // would otherwise stay armed and retain the session.
session.setTimeout(0);
session.removeAllListeners('timeout');
+ state.flags |= SESSION_FLAGS_DESTROYED;
+ state.destroyCode = code;
+
const socket = session[kSocket];
const handle = session[kHandle];
diff --git a/test/parallel/test-http2-session-timeout-cleared-on-close-raw-socket.js b/test/parallel/test-http2-session-timeout-cleared-on-close-raw-socket.js
new file mode 100644
index 00000000000..1df70ec9b52
--- /dev/null
+++ b/test/parallel/test-http2-session-timeout-cleared-on-close-raw-socket.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const net = require('net');
+
+// Same as test-http2-session-timeout-cleared-on-close.js, but the client's
+// TCP socket is destroyed without sending GOAWAY.
+const server = http2.createServer();
+server.setTimeout(30_000, common.mustNotCall());
+
+server.on('session', common.mustCall((session) => {
+ session.on('goaway', common.mustNotCall());
+ session.on('close', common.mustCall(() => {
+ assert.strictEqual(session.timeout, 0);
+ server.close();
+ }));
+}));
+
+server.on('stream', common.mustCall((stream) => {
+ stream.respond({ ':status': 200 });
+ stream.end('ok');
+}));
+
+server.listen(0, common.mustCall(() => {
+ let socket;
+ const client = http2.connect(`http://localhost:${server.address().port}`, {
+ createConnection: (url) => (socket = net.connect(url.port, url.hostname)),
+ });
+ client.on('error', () => {});
+ const req = client.request();
+ req.on('error', () => {});
+ req.resume();
+ req.on('end', common.mustCall(() => socket.destroy()));
+}));
diff --git a/test/parallel/test-http2-session-timeout-cleared-on-close.js b/test/parallel/test-http2-session-timeout-cleared-on-close.js
new file mode 100644
index 00000000000..476e60702ab
--- /dev/null
+++ b/test/parallel/test-http2-session-timeout-cleared-on-close.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+
+// A destroyed session must not keep its idle timer armed: setTimeout() is a
+// no-op once the session is marked destroyed, so closeSession() has to clear
+// the timer first.
+const server = http2.createServer();
+server.setTimeout(30_000, common.mustNotCall());
+
+server.on('session', common.mustCall((session) => {
+ session.on('close', common.mustCall(() => {
+ assert.strictEqual(session.timeout, 0);
+ server.close();
+ }));
+}));
+
+server.on('stream', common.mustCall((stream) => {
+ stream.respond({ ':status': 200 });
+ stream.end('ok');
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+ req.resume();
+ req.on('end', common.mustCall(() => client.close()));
+}));