Skip to content

Commit 407630a

Browse files
[v6.x] fix(fetch): fix aborting Streams (#3754) (#6000)
* fix aborting Streams (#3754) * fix aborting Streams * stop memory leak (cherry picked from commit afeb626) * test: add regression test for invalid compressed fetch body A response that advertises content-encoding: gzip but delivers a body that is not valid gzip in a later packet than the headers left the body stream pending forever, because the 'terminated' listener had already been removed in onComplete. Refs: #5999 --------- Co-authored-by: Austin Henrie <113467168+epistemancering@users.noreply.github.com>
1 parent e1d0501 commit 407630a

3 files changed

Lines changed: 55 additions & 6 deletions

File tree

‎lib/web/fetch/index.js‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,8 +1950,10 @@ async function httpNetworkFetch (
19501950
// 19. Run these steps in parallel:
19511951

19521952
// 1. Run these steps, but abort when fetchParams is canceled:
1953-
fetchParams.controller.onAborted = onAborted
1954-
fetchParams.controller.on('terminated', onAborted)
1953+
if (!fetchParams.controller.resume) {
1954+
fetchParams.controller.on('terminated', onAborted)
1955+
}
1956+
19551957
fetchParams.controller.resume = async () => {
19561958
// 1. While true
19571959
while (true) {
@@ -2218,10 +2220,6 @@ async function httpNetworkFetch (
22182220
fetchParams.controller.off('terminated', this.abort)
22192221
}
22202222

2221-
if (fetchParams.controller.onAborted) {
2222-
fetchParams.controller.off('terminated', fetchParams.controller.onAborted)
2223-
}
2224-
22252223
fetchParams.controller.ended = true
22262224

22272225
this.body.push(null)

‎test/fetch/encoding.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,27 @@ describe('content-encoding chain limit', () => {
122122
)
123123
})
124124
})
125+
126+
// https://github.com/nodejs/undici/issues/5999
127+
test('invalid compressed body received after the headers rejects instead of hanging', async (t) => {
128+
const server = createServer((req, res) => {
129+
res.writeHead(200, {
130+
'Content-Encoding': 'gzip',
131+
'Content-Length': '16'
132+
})
133+
res.flushHeaders()
134+
135+
setTimeout(() => res.end(Buffer.alloc(16)), 100)
136+
}).listen(0, '127.0.0.1')
137+
138+
t.after(closeServerAsPromise(server))
139+
await once(server, 'listening')
140+
141+
const response = await fetch(`http://127.0.0.1:${server.address().port}`, { keepalive: false })
142+
143+
await assert.rejects(response.text(), (err) => {
144+
assert.strictEqual(err.name, 'TypeError')
145+
assert.strictEqual(err.cause?.code, 'Z_DATA_ERROR')
146+
return true
147+
})
148+
})

‎test/fetch/issue-1711.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,30 @@ test('Redirecting a bunch does not cause a MaxListenersExceededWarning', async (
3131

3232
assert.deepStrictEqual(response.url, `${url}/${redirects - 1}`)
3333
})
34+
35+
test(
36+
'aborting a Stream throws',
37+
() => {
38+
return new Promise((resolve, reject) => {
39+
const httpServer = createServer((request, response) => {
40+
response.end(new Uint8Array(20000))
41+
}).listen(async () => {
42+
const serverAddress = httpServer.address()
43+
44+
if (typeof serverAddress === 'object') {
45+
const abortController = new AbortController()
46+
const readStream = (await fetch(`http://localhost:${serverAddress?.port}`, { signal: abortController.signal })).arrayBuffer()
47+
abortController.abort()
48+
setTimeout(reject)
49+
50+
try {
51+
await readStream
52+
} catch {
53+
httpServer.close()
54+
resolve()
55+
}
56+
}
57+
})
58+
})
59+
}
60+
)

0 commit comments

Comments
 (0)