Commit 80a74e1813d for nodejs
commit 80a74e1813ddbb5b0bd507e1e09bc02790c452a2
Author: Yuya Inoue <65857152+inoway46@users.noreply.github.com>
Date: Sun Nov 1 01:26:28 2026 +0900
http2: release queued data memory on stream cancellation
Release the memory charged for queued outbound data when a stream is
canceled, so subsequent requests can reuse the session's memory budget.
Refs: https://github.com/nodejs/node/issues/66739
Assisted-by: Codex
Signed-off-by: inoway46 <inoueyuya416@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/67113
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 6aa389536f1..dda33643788 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -2461,6 +2461,8 @@ void Http2Stream::CompleteDestroyCleanup() {
// Free any remaining outgoing data chunks here. This should be done
// here because it's possible for destroy to have been called while
// we still have queued outbound writes.
+ if (session_)
+ DecrementAvailableOutboundLength(available_outbound_length_);
while (!queue_.empty()) {
NgHttp2StreamWrite& head = queue_.front();
if (head.req_wrap)
diff --git a/test/parallel/test-http2-max-session-memory-cancel.js b/test/parallel/test-http2-max-session-memory-cancel.js
new file mode 100644
index 00000000000..a333587dc3c
--- /dev/null
+++ b/test/parallel/test-http2-max-session-memory-cancel.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const http2 = require('http2');
+const { once } = require('events');
+
+(async () => {
+ // Keep the entire upload queued until it is canceled.
+ const server = http2.createServer({ settings: { initialWindowSize: 0 } });
+ server.on('stream', common.mustCall((stream, headers) => {
+ stream.on('error', () => {});
+ if (headers[':path'] === '/cancel') return;
+ stream.respond({ ':status': 200 });
+ stream.end();
+ }, 2));
+ server.listen(0);
+ await once(server, 'listening');
+
+ const client = http2.connect(`http://localhost:${server.address().port}`, {
+ maxSessionMemory: 1,
+ });
+ await once(client, 'remoteSettings');
+ const uploadArrived = once(server, 'stream');
+ const upload = client.request({ ':method': 'POST', ':path': '/cancel' });
+ upload.on('response', common.mustNotCall());
+ upload.end(Buffer.alloc(4 * 1024 * 1024));
+ await uploadArrived;
+ const closed = once(upload, 'close');
+ upload.destroy();
+ await closed;
+ // Native write queue cleanup runs on an immediate after stream destruction.
+ await new Promise(setImmediate);
+
+ // The canceled upload must no longer count towards maxSessionMemory.
+ const req = client.request({ ':path': '/check' });
+ const response = once(req, 'response');
+ req.end();
+ const [headers] = await response;
+ assert.strictEqual(headers[':status'], 200);
+ const ended = once(req, 'end');
+ req.resume();
+ await ended;
+ client.close();
+ server.close();
+})().then(common.mustCall());