Repository navigation
Node 22 fetch hangs forever on a broken gzip body #5999
Description
Bug Description
In short: When a response header specifies content-encoding: gzip but the body is not valid gzip, and the body arrives in a later packet than the headers, Node 22's built-in fetch never settles text(), json() or a stream read. The request's AbortSignal can't end it either.
It's an undici regression introduced in 6.7.0 and still present in 6.29.0, the latest 6.x release. undici 7 and 8 (bundled in Node 24 and 26) are not affected.
Reproduction
Standalone reproduction script:
'use strict'
const net = require("node:net");
const server = net.createServer(socket => socket.once("data", () => {
socket.write("HTTP/1.1 200 OK\r\ncontent-encoding: gzip\r\ncontent-length: 16\r\n\r\n");
setTimeout(() => socket.write(Buffer.alloc(16)), 100);
}));
server.listen(0, "127.0.0.1", async () => {
const label = `node ${process.version} (undici ${process.versions.undici})`;
const watchdog = setTimeout(() => { console.log(`${label}: HANG`); process.exit(1); }, 3000);
try {
const res = await fetch(`http://127.0.0.1:${server.address().port}/`);
await res.text();
console.log(`${label}: resolved (unexpected)`);
} catch (error) {
console.log(`${label}: rejected - ${error.message} (${error.cause?.code})`);
}
clearTimeout(watchdog);
process.exit(0);
});Steps to reproduce (if not using the script above):
If we run the above script on affected versions it prints node {version} (undici {version}): HANG
Expected Behavior
The promise should resolve and the script should print resolved or rejected but not HANG.
Actual Behavior
The promise hangs and never resolves.
In undici 6.7.0 – 6.29.0, lib/web/fetch/index.js has this in the dispatcher's onComplete:
if (fetchParams.controller.onAborted) {
fetchParams.controller.off('terminated', fetchParams.controller.onAborted)
}
onAborted is the listener that errors the response's stream with TypeError('terminated'). The bug unfolds like this:
onCompletefires as soon as the raw (still compressed) bytes have arrived.- Decompression happens later, inside the body pull loop. When zlib fails with
Z_DATA_ERROR, the pull loop callscontroller.terminate(err). - Nobody is listening for
terminatedany more, so the response stream is never errored. text(),json()and readers wait forever. The request'sAbortSignalcan't help, because it reaches the stream through the same removed listener.
undici 7.0.0 doesn't have those lines.
If we delete these lines from a copy of 6.29.0, the repro rejects correctly, and the unpatched copy still hangs.
So the fix is a back port of the 7.x behaviour to the 6.x branch.
Logs & Screenshots
N/A
Environment
- OS: macOS 26.3
- Node.js version: 22.23.2
- undici version: 6.28.0
Additional context
N/A
Activity
test: add regression test for invalid compressed fetch body
fix(fetch): error the stream when decompression fails after onComplete