Skip to content

Node 22 fetch hangs forever on a broken gzip body #5999

Open

Description

@zaitsman

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:

  • onComplete fires 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 calls controller.terminate(err).
  • Nobody is listening for terminated any more, so the response stream is never errored.
  • text(), json() and readers wait forever. The request's AbortSignal can'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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions