Skip to content

Commit a1846e5

Browse files
authored
fetch: fix leak (#2049)
1 parent 816dcaa commit a1846e5

File tree

4 files changed

+59
-5
lines changed

4 files changed

+59
-5
lines changed

Diff for: lib/fetch/request.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const { setMaxListeners, getEventListeners, defaultMaxListeners } = require('eve
3434
let TransformStream = globalThis.TransformStream
3535

3636
const kInit = Symbol('init')
37+
const kAbortController = Symbol('abortController')
3738

3839
const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
3940
signal.removeEventListener('abort', abort)
@@ -354,20 +355,30 @@ class Request {
354355
if (signal.aborted) {
355356
ac.abort(signal.reason)
356357
} else {
358+
// Keep a strong ref to ac while request object
359+
// is alive. This is needed to prevent AbortController
360+
// from being prematurely garbage collected.
361+
// See, https://github.com/nodejs/undici/issues/1926.
362+
this[kAbortController] = ac
363+
364+
const acRef = new WeakRef(ac)
357365
const abort = function () {
358-
ac.abort(this.reason)
366+
const ac = acRef.deref()
367+
if (ac !== undefined) {
368+
ac.abort(this.reason)
369+
}
359370
}
360371

361372
// Third-party AbortControllers may not work with these.
362-
// See https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619
373+
// See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
363374
try {
364375
if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
365376
setMaxListeners(100, signal)
366377
}
367378
} catch {}
368379

369380
signal.addEventListener('abort', abort, { once: true })
370-
requestFinalizer.register(this, { signal, abort })
381+
requestFinalizer.register(ac, { signal, abort })
371382
}
372383
}
373384

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && tsd",
5050
"test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js",
5151
"test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch",
52-
"test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js && tap test/webidl/*.js)",
52+
"test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)",
5353
"test:jest": "node scripts/verifyVersion.js 14 || jest",
5454
"test:tap": "tap test/*.js test/diagnostics-channel/*.js",
5555
"test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",

Diff for: test/client-keep-alive.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ test('keep-alive header', (t) => {
3232
body.on('end', () => {
3333
const timeout = setTimeout(() => {
3434
t.fail()
35-
}, 3e3)
35+
}, 4e3)
3636
client.on('disconnect', () => {
3737
t.pass()
3838
clearTimeout(timeout)

Diff for: test/fetch/fetch-leak.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const { fetch } = require('../..')
5+
const { createServer } = require('http')
6+
7+
test('do not leak', (t) => {
8+
t.plan(1)
9+
10+
const server = createServer((req, res) => {
11+
res.end()
12+
})
13+
t.teardown(server.close.bind(server))
14+
15+
let url
16+
let done = false
17+
server.listen(0, function attack () {
18+
if (done) {
19+
return
20+
}
21+
url ??= new URL(`http://127.0.0.1:${server.address().port}`)
22+
const controller = new AbortController()
23+
fetch(url, { signal: controller.signal })
24+
.then(res => res.arrayBuffer())
25+
.then(attack)
26+
})
27+
28+
let prev = Infinity
29+
let count = 0
30+
const interval = setInterval(() => {
31+
done = true
32+
global.gc()
33+
const next = process.memoryUsage().heapUsed
34+
if (next <= prev) {
35+
t.pass()
36+
} else if (count++ > 10) {
37+
t.fail()
38+
} else {
39+
prev = next
40+
}
41+
}, 1e3)
42+
t.teardown(() => clearInterval(interval))
43+
})

0 commit comments

Comments
 (0)