forked from nodejs/node
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest-http2-socket-close-zombie-session.js
More file actions
101 lines (85 loc) Β· 3.11 KB
/
test-http2-socket-close-zombie-session.js
File metadata and controls
101 lines (85 loc) Β· 3.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Flags: --expose-internals
'use strict';
// Regression test for https://github.com/nodejs/node/issues/61304
// When the underlying socket is closed at the OS level without
// sending RST/FIN (e.g., network black hole), the HTTP/2 session
// enters a zombie state where it believes the connection is alive
// but the socket is actually dead. Subsequent write attempts should
// fail gracefully rather than crash with assertion failures.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
const { kSocket } = require('internal/http2/util');
const fixtures = require('../common/fixtures');
const server = h2.createSecureServer({
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem')
});
server.on('stream', common.mustCall((stream) => {
stream.respond({ ':status': 200 });
stream.end('hello');
}));
server.listen(0, common.mustCall(() => {
const client = h2.connect(`https://localhost:${server.address().port}`, {
rejectUnauthorized: false
});
let cleanupTimer;
const cleanup = () => {
clearTimeout(cleanupTimer);
if (!client.destroyed) {
client.destroy();
}
if (server.listening) {
server.close();
}
};
// Verify session eventually closes
client.on('close', common.mustCall(() => {
cleanup();
}));
// Handle errors without failing test
client.on('error', () => {
// Expected - connection closed
});
// First request to establish connection
const req1 = client.request({ ':path': '/' });
req1.on('response', common.mustCall());
req1.on('data', () => {});
req1.on('end', common.mustCall(() => {
// Connection is established, now simulate network black hole
// by destroying the underlying socket without proper close
const socket = client[kSocket];
// Verify session state before socket destruction
assert.strictEqual(client.closed, false);
assert.strictEqual(client.destroyed, false);
// Destroy the socket to simulate OS-level connection loss
// This mimics what happens when network drops packets without RST/FIN
socket.destroy();
// The session should handle this gracefully
// Prior to fix: this would cause assertion failures in subsequent writes
// After fix: session should close properly
setImmediate(() => {
// Try to send another request into the zombie session
// With the fix, the session should close gracefully without crashing
try {
const req2 = client.request({ ':path': '/test' });
// The request may or may not emit an error event,
// but it should not receive a response
req2.on('error', () => {
// Acceptable: error event fires
});
req2.on('response', common.mustNotCall(
'Should not receive response from zombie session'
));
req2.end();
} catch {
// Also acceptable: synchronous error on request creation
}
// Fallback cleanup if close event doesn't fire within 100ms
cleanupTimer = setTimeout(cleanup, 100);
});
}));
req1.end();
}));