Skip to content

Commit a7569c8

Browse files
fatihguzeldevaduh95
authored andcommitted
test: extend http2 cancellation coverage
Cover session reuse after canceling queued uploads, including partial sends and destruction from a write callback. Verify that another stream remains usable after cancellation. Signed-off-by: Fatih Güzel <fatihguzeldev@gmail.com> PR-URL: #66784 Fixes: #66742 Reviewed-By: Tim Perry <pimterry@gmail.com>
1 parent 605b1f9 commit a7569c8

1 file changed

Lines changed: 110 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
const assert = require('assert');
7+
const { once } = require('events');
8+
const http2 = require('http2');
9+
const { setImmediate } = require('timers/promises');
10+
11+
const { NGHTTP2_CANCEL, NGHTTP2_NO_ERROR } = http2.constants;
12+
13+
// Canceling a queued upload must release its remaining session memory credit,
14+
// without disrupting another stream or preventing new requests.
15+
async function test(initialWindowSize, destroyInWriteCallback = false) {
16+
const server = http2.createServer({ settings: { initialWindowSize } });
17+
const sessions = new Set();
18+
let survivingStream;
19+
let received = 0;
20+
server.on('session', (session) => sessions.add(session));
21+
server.on('stream', common.mustCall((stream, headers) => {
22+
if (headers[':method'] === 'POST') {
23+
if (destroyInWriteCallback) {
24+
stream.on('error', common.mustCall((error) => {
25+
assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_ABORTED');
26+
}));
27+
} else if (initialWindowSize === 0) {
28+
stream.close(NGHTTP2_CANCEL);
29+
} else {
30+
stream.once('data', common.mustCall((chunk) => {
31+
received += chunk.length;
32+
stream.close(NGHTTP2_CANCEL);
33+
}));
34+
}
35+
return;
36+
}
37+
stream.respond({ ':status': 200 });
38+
if (headers[':path'] === '/survivor') {
39+
survivingStream = stream;
40+
} else {
41+
stream.end();
42+
survivingStream.end('still alive');
43+
}
44+
}, 3));
45+
46+
let client;
47+
try {
48+
server.listen(0, '127.0.0.1');
49+
await once(server, 'listening');
50+
client = http2.connect(`http://127.0.0.1:${server.address().port}`, {
51+
maxSessionMemory: 1,
52+
});
53+
const [settings] = await once(client, 'remoteSettings');
54+
assert.strictEqual(settings.initialWindowSize, initialWindowSize);
55+
56+
const survivor = client.request({ ':path': '/survivor' });
57+
let body = '';
58+
survivor.setEncoding('utf8');
59+
survivor.on('data', (chunk) => { body += chunk; });
60+
const survived = once(survivor, 'end');
61+
const [headers] = await once(survivor, 'response');
62+
assert.strictEqual(headers[':status'], 200);
63+
64+
const upload = client.request({ ':method': 'POST' });
65+
if (destroyInWriteCallback) {
66+
upload.on('error', common.mustNotCall());
67+
// Completing this empty native write dispatches the buffered body before
68+
// its user callback destroys the stream, while DATA is being gathered.
69+
upload.write('', common.mustCall(() => upload.destroy()));
70+
} else {
71+
upload.on('error', common.mustCall((error) => {
72+
assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_ABORTED');
73+
}));
74+
}
75+
const closed = new Promise((resolve) => upload.once('close', resolve));
76+
upload.write(Buffer.alloc(4 * 1024 * 1024), common.mustCall());
77+
await closed;
78+
assert.strictEqual(upload.rstCode,
79+
destroyInWriteCallback ? NGHTTP2_NO_ERROR : NGHTTP2_CANCEL);
80+
if (initialWindowSize === 0) {
81+
assert.strictEqual(received, 0);
82+
} else if (!destroyInWriteCallback) {
83+
assert(received > 0);
84+
assert(received < 4 * 1024 * 1024);
85+
}
86+
87+
// Native stream destruction frees the queued write in an immediate.
88+
await setImmediate();
89+
const request = client.request();
90+
const completed = Promise.all([
91+
once(request, 'response'),
92+
once(request, 'end'),
93+
]);
94+
request.resume();
95+
const [[responseHeaders]] = await completed;
96+
assert.strictEqual(responseHeaders[':status'], 200);
97+
await survived;
98+
assert.strictEqual(body, 'still alive');
99+
} finally {
100+
client?.destroy();
101+
for (const session of sessions) session.destroy();
102+
server.close();
103+
}
104+
}
105+
106+
(async () => {
107+
await test(0);
108+
await test(64 * 1024);
109+
await test(64 * 1024, true);
110+
})().then(common.mustCall());

0 commit comments

Comments
 (0)