Skip to content

fix(fetch): keep cloned request abort linkage alive across GC - #5768

Open
pacocartones wants to merge 1 commit into
nodejs:mainfrom
pacocartones:fix/4068-clone-abort-gc
Open

fix(fetch): keep cloned request abort linkage alive across GC#5768
pacocartones wants to merge 1 commit into
nodejs:mainfrom
pacocartones:fix/4068-clone-abort-gc

Conversation

@pacocartones

Copy link
Copy Markdown
Contributor

Fixes #4068.

Problem

A cloned Request loses abort propagation after garbage collection. The clone-follow wiring (from #3169) made the dependent controller reachable only through WeakRefs, so GC collects it and acRef.deref() returns undefined, silently dropping propagation. Deeper: a clone follows the source request's internal signal, whose controller is held only by the source Request object; when the source is dropped (req = req.clone()), that controller is collected too. Same for new Request(existingRequest).

Confirmed still reproducing at HEAD (undici 8.10.1): the repro fetch hangs because aborting the original after a GC never rejects the clone.

Fix

Mirror the constructor's proven strong-reference strategy (#1926) for the follow linkage, without altering the abort wiring (preserving #3169's firing order) and without adding a FinalizationRegistry to clones (respecting #4320):

  1. clone() keeps the dependent controller alive (clonedRequestObject[kAbortController] = ac).
  2. Both clone() and the constructor keep the flattened source controller chain alive via a new kAbortSourceControllers symbol, so arbitrarily deep clone chains survive even when every intermediate Request is collected. Refs release when the follower itself is GC'd (no leak — verified against long-lived-abort-controller.js).

Test

test/issue-4068.js (run under node --expose-gc, deterministic forceGc() with 10 passes): both clone() and new Request(req) cases — RED time out (hang); GREEN 3/3 pass in ~150ms. fetch/request 32, fetch/abort, request-signal, long-lived-abort-controller all green; npm run lint green.

A cloned (or constructor-copied) Request follows the source request's
internal AbortSignal. That linkage was reachable only through weak
references: the dependent controller lived in a WeakRef inside
dependentControllerMap and in the abort listener closure, and the source
request's controller was reachable only while the source request object
stayed alive. Once the source request was dropped and a GC ran, the
controller chain was collected and aborting the original controller no
longer aborted the clone, so the in-flight fetch hung until its timeout.

Mirror the constructor's nodejs#1926 strong-reference approach for the follow
linkage: keep the dependent controller alive on the clone, and keep the
source request's (flattened) controller chain alive for the follower's
lifetime. This covers Request.clone(), new Request(request), and
arbitrarily deep chains, without reintroducing a FinalizationRegistry on
clones and without changing the abort firing order from nodejs#3169.

Fixes: nodejs#4068
@codecov-commenter

codecov-commenter commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.52%. Comparing base (17117f6) to head (45daddf).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5768      +/-   ##
==========================================
+ Coverage   93.50%   93.52%   +0.02%     
==========================================
  Files         110      110              
  Lines       39072    39141      +69     
==========================================
+ Hits        36534    36608      +74     
+ Misses       2538     2533       -5     

☔ 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.

Sign up for free to 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.

Cloned requests loose abort signal on GC

2 participants