fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(h2): release completed requests from queue - #5569

Merged
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention
Jul 20, 2026
Merged

fix(h2): release completed requests from queue#5569
mcollina merged 4 commits into
nodejs:mainfrom
staylor:fix/h2-abort-queue-retention

Conversation

@staylor

Copy link
Copy Markdown
Contributor

This relates to...

Fixes#5566.

Rationale

The HTTP/2 in-order completion optimization introduced in #5483 advanced kRunningIdx without clearing the completed request's queue slot. Until the periodic queue compaction ran, each dead prefix slot retained its Request, whose controller retained the ClientHttp2Stream and the rest of the fetch graph. Under sustained HTTP/2 abort churn this retained native response buffers and caused memory growth. A 20,000-request reproduction at 50% aborts grew the heap by 10.6 MB before this change and 2.8 MB after it, matching the pre-#5483 baseline.

A stream can also close after its request has already been marked aborted. Terminal stream cleanup must still finalize that request even though onResponseEnd must not run.

Changes

Features

N/A

Bug Fixes

  • Clear an in-order completed HTTP/2 request's queue slot before advancing kRunningIdx, preserving the O(1) fast path and matching the existing HTTP/1 queue behavior.
  • Finalize terminal HTTP/2 streams independently of whether the response ended successfully.
  • Add regression coverage for an already-aborted request closing without response completion.

Breaking Changes and Deprecations

N/A

Status

The HTTP/2 in-order completion fast path advanced the running index but retained the completed Request in its queue slot until batched compaction. Under sustained abort churn, each retained Request pinned its stream and fetch graph, including native response buffers.
Clear the completed slot before advancing, preserving the O(1) fast path, and finalize closed streams even when the Request was already marked aborted.
Fixes: nodejs#5566
Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/4ad398e1-8641-4e85-a62d-cb78992a7aae
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (87270e4) to head (7ffd5c3).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #5569 +/- ##
=======================================
Coverage 93.47% 93.47% =======================================
Files 110 110 Lines 37507 37561 +54 =======================================
+ Hits 35059 35111 +52 - Misses 2448 2450 +2 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina
mcollina requested a review from trivikrJuly 17, 2026 15:02

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95July 17, 2026 15:38
@mcollina

Copy link
Copy Markdown
Member

I'm relatively concerned by this PR (and previous ones) that do not actually provide e2e tests.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/7d65db46-a09e-471a-9e1c-abdaa5accba8
@staylor

Copy link
Copy Markdown
ContributorAuthor

Thanks @mcollina — agreed. I’ve added an end-to-end regression test in 84ab33d. It uses a real TLS HTTP/2 server through the public fetch/Client APIs, aborts an in-flight response while keeping the session open and usable, and verifies that the aborted fetch graph is released. The test fails against the parent commit and passes with this fix.

@metcoder95metcoder95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small comment; rest lgtm

Comment threadtest/fetch/issue-5566.js Outdated

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

mcollinaand others added 2 commits July 20, 2026 04:07
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina merged commit f87273e into nodejs:mainJul 20, 2026
38 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
mcollina added a commit that referenced this pull request Jul 30, 2026
…#5618)
Backport of #5410 (and the fast path from #5569).
HTTP/2 completes out of order, but every completion site advanced the
running index blindly:
client[kQueue][client[kRunningIdx]++] = null
so whichever request happened to sit at the head was retired instead of the
one that actually finished. With two streams in flight, completing the
second one clears the first's slot while the second stays in the running
window for good:
after /second queue=[null, "/second"] runningIdx=1 pendingIdx=2
after /third queue=[null, null, "/third"] runningIdx=2 pendingIdx=3
The still-running /first is gone from the queue and two finished requests
are counted as running forever, so kRunning never returns to zero. Since
_resume() stops dispatching once kRunning reaches the concurrency limit, a
client accumulating these eventually stops sending anything.
Port completeRequest() from main: retire the request by identity, keeping
the O(1) in-order fast path, and splice it out when it finished out of
order. Cleared slots can now appear in the queue, so the paths that walk it
skip them, as they do on main.
Refs: #5404
Refs: #5410
Refs: #5569
Claude-Session: https://claude.ai/code/session_01A49JamgF2TkZHu5h58ChUM
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: aborted HTTP/2 requests strand the response body and leak the request graph (regression in #5483 / 8.6.0)

4 participants

@staylor@codecov-commenter@mcollina@metcoder95