Commit c085224f33e for nodejs
commit c085224f33efd7938fb267efff52ddf41cad7a9a
Author: fatih <fatihguzeldev@gmail.com>
Date: Wed Nov 4 05:23:42 2026 +0300
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: https://github.com/nodejs/node/pull/66784
Fixes: https://github.com/nodejs/node/issues/66742
Reviewed-By: Tim Perry <pimterry@gmail.com>
diff --git a/test/parallel/test-http2-session-memory-after-cancel.js b/test/parallel/test-http2-session-memory-after-cancel.js
new file mode 100644
index 00000000000..4c36729f0e0
--- /dev/null
+++ b/test/parallel/test-http2-session-memory-after-cancel.js
@@ -0,0 +1,110 @@
+'use strict';
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const { once } = require('events');
+const http2 = require('http2');
+const { setImmediate } = require('timers/promises');
+
+const { NGHTTP2_CANCEL, NGHTTP2_NO_ERROR } = http2.constants;
+
+// Canceling a queued upload must release its remaining session memory credit,
+// without disrupting another stream or preventing new requests.
+async function test(initialWindowSize, destroyInWriteCallback = false) {
+ const server = http2.createServer({ settings: { initialWindowSize } });
+ const sessions = new Set();
+ let survivingStream;
+ let received = 0;
+ server.on('session', (session) => sessions.add(session));
+ server.on('stream', common.mustCall((stream, headers) => {
+ if (headers[':method'] === 'POST') {
+ if (destroyInWriteCallback) {
+ stream.on('error', common.mustCall((error) => {
+ assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_ABORTED');
+ }));
+ } else if (initialWindowSize === 0) {
+ stream.close(NGHTTP2_CANCEL);
+ } else {
+ stream.once('data', common.mustCall((chunk) => {
+ received += chunk.length;
+ stream.close(NGHTTP2_CANCEL);
+ }));
+ }
+ return;
+ }
+ stream.respond({ ':status': 200 });
+ if (headers[':path'] === '/survivor') {
+ survivingStream = stream;
+ } else {
+ stream.end();
+ survivingStream.end('still alive');
+ }
+ }, 3));
+
+ let client;
+ try {
+ server.listen(0, '127.0.0.1');
+ await once(server, 'listening');
+ client = http2.connect(`http://127.0.0.1:${server.address().port}`, {
+ maxSessionMemory: 1,
+ });
+ const [settings] = await once(client, 'remoteSettings');
+ assert.strictEqual(settings.initialWindowSize, initialWindowSize);
+
+ const survivor = client.request({ ':path': '/survivor' });
+ let body = '';
+ survivor.setEncoding('utf8');
+ survivor.on('data', (chunk) => { body += chunk; });
+ const survived = once(survivor, 'end');
+ const [headers] = await once(survivor, 'response');
+ assert.strictEqual(headers[':status'], 200);
+
+ const upload = client.request({ ':method': 'POST' });
+ if (destroyInWriteCallback) {
+ upload.on('error', common.mustNotCall());
+ // Completing this empty native write dispatches the buffered body before
+ // its user callback destroys the stream, while DATA is being gathered.
+ upload.write('', common.mustCall(() => upload.destroy()));
+ } else {
+ upload.on('error', common.mustCall((error) => {
+ assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_ABORTED');
+ }));
+ }
+ const closed = new Promise((resolve) => upload.once('close', resolve));
+ upload.write(Buffer.alloc(4 * 1024 * 1024), common.mustCall());
+ await closed;
+ assert.strictEqual(upload.rstCode,
+ destroyInWriteCallback ? NGHTTP2_NO_ERROR : NGHTTP2_CANCEL);
+ if (initialWindowSize === 0) {
+ assert.strictEqual(received, 0);
+ } else if (!destroyInWriteCallback) {
+ assert(received > 0);
+ assert(received < 4 * 1024 * 1024);
+ }
+
+ // Native stream destruction frees the queued write in an immediate.
+ await setImmediate();
+ const request = client.request();
+ const completed = Promise.all([
+ once(request, 'response'),
+ once(request, 'end'),
+ ]);
+ request.resume();
+ const [[responseHeaders]] = await completed;
+ assert.strictEqual(responseHeaders[':status'], 200);
+ await survived;
+ assert.strictEqual(body, 'still alive');
+ } finally {
+ client?.destroy();
+ for (const session of sessions) session.destroy();
+ server.close();
+ }
+}
+
+(async () => {
+ await test(0);
+ await test(64 * 1024);
+ await test(64 * 1024, true);
+})().then(common.mustCall());