Skip to content

Commit 020578f

Browse files
http2: fix zombie session crash on socket close
1 parent 0da120f commit 020578f

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

src/node_http2.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,6 +2095,11 @@ void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
20952095
if (nread <= 0) {
20962096
if (nread < 0) {
20972097
PassReadErrorToPreviousListener(nread);
2098+
// Socket has encountered an error or EOF. Close the session to prevent
2099+
// zombie state where the session believes the connection is alive but
2100+
// the underlying socket is dead. This prevents assertion failures in
2101+
// subsequent write attempts. (Ref: https://github.com/nodejs/node/issues/61304)
2102+
Close(NGHTTP2_NO_ERROR, true);
20982103
}
20992104
return;
21002105
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
// Regression test for https://github.com/nodejs/node/issues/61304
5+
// When the underlying socket is closed at the OS level without
6+
// sending RST/FIN (e.g., network black hole), the HTTP/2 session
7+
// enters a zombie state where it believes the connection is alive
8+
// but the socket is actually dead. Subsequent write attempts should
9+
// fail gracefully rather than crash with assertion failures.
10+
11+
const common = require('../common');
12+
if (!common.hasCrypto)
13+
common.skip('missing crypto');
14+
15+
const assert = require('assert');
16+
const h2 = require('http2');
17+
const { kSocket } = require('internal/http2/util');
18+
const fixtures = require('../common/fixtures');
19+
20+
const server = h2.createSecureServer({
21+
key: fixtures.readKey('agent1-key.pem'),
22+
cert: fixtures.readKey('agent1-cert.pem')
23+
});
24+
25+
server.on('stream', common.mustCall((stream) => {
26+
stream.respond({ ':status': 200 });
27+
stream.end('hello');
28+
}));
29+
30+
server.listen(0, common.mustCall(() => {
31+
const client = h2.connect(`https://localhost:${server.address().port}`, {
32+
rejectUnauthorized: false
33+
});
34+
35+
let firstRequestDone = false;
36+
37+
// First request to establish connection
38+
const req1 = client.request({ ':path': '/' });
39+
req1.on('response', common.mustCall(() => {
40+
firstRequestDone = true;
41+
}));
42+
req1.on('data', () => {});
43+
req1.on('end', common.mustCall(() => {
44+
// Connection is established, now simulate network black hole
45+
// by destroying the underlying socket without proper close
46+
const socket = client[kSocket];
47+
48+
// Verify session state before socket destruction
49+
assert.strictEqual(client.closed, false);
50+
assert.strictEqual(client.destroyed, false);
51+
52+
// Destroy the socket to simulate OS-level connection loss
53+
// This mimics what happens when network drops packets without RST/FIN
54+
socket.destroy();
55+
56+
// The session should handle this gracefully
57+
// Prior to fix: this would cause assertion failures in subsequent writes
58+
// After fix: session should close properly
59+
60+
setImmediate(() => {
61+
// Try to send another request into the zombie session
62+
// With the fix, the session should close gracefully without crashing
63+
try {
64+
const req2 = client.request({ ':path': '/test' });
65+
// The request may or may not emit an error event,
66+
// but it should not receive a response
67+
req2.on('error', () => {
68+
// Acceptable: error event fires
69+
});
70+
req2.on('response', common.mustNotCall(
71+
'Should not receive response from zombie session'
72+
));
73+
req2.end();
74+
} catch (err) {
75+
// Also acceptable: synchronous error on request creation
76+
assert(err);
77+
}
78+
79+
// Verify session eventually closes
80+
client.on('close', common.mustCall(() => {
81+
server.close();
82+
}));
83+
84+
// Force cleanup if session doesn't close naturally
85+
setTimeout(() => {
86+
if (!client.destroyed) {
87+
client.destroy();
88+
}
89+
if (server.listening) {
90+
server.close();
91+
}
92+
}, 1000);
93+
});
94+
}));
95+
96+
req1.end();
97+
}));

0 commit comments

Comments
 (0)