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());