Repository navigation
Node 24.16.0: extract-zip hangs extracting #63487
Copy link
Copy link
Closed
Description
Version
v24.16.0
Platform
Linux, Docker image `node:24.16.0-slim`
Subsystem
No response
What steps will reproduce the bug?
#!/usr/bin/env bash
set -euo pipefail
for NODE_VERSION in 24.15.0 24.16.0; do
echo "### Testing Node ${NODE_VERSION}"
docker run --rm "node:${NODE_VERSION}-slim" sh -lc '
set -eu
apt-get update >/dev/null
apt-get install -y curl >/dev/null
mkdir -p /repro /tmp/out-node
cd /repro
npm init -y >/dev/null 2>&1
npm install extract-zip@2.0.1 >/dev/null 2>&1
curl -fsSL -o chrome.zip https://storage.googleapis.com/chrome-for-testing-public/134.0.6998.35/linux64/chrome-linux64.zip
node --input-type=module -e "import extract from \"extract-zip\"; await extract(\"chrome.zip\", { dir: \"/tmp/out-node\" }); console.log(\"extract-zip done\")"
test -x /tmp/out-node/chrome-linux64/chrome
echo "extract-zip: PASS"
'
done
How often does it reproduce? Is there a required condition?
Always with v24.16.0, never with v24.15.0
What is the expected behavior? Why is that the expected behavior?
Both Node versions should complete extraction successfully
What do you see instead?
### Testing Node 24.15.0
debconf: delaying package configuration, since apt-utils is not installed
extract-zip done
extract-zip: PASS
### Testing Node 24.16.0
debconf: delaying package configuration, since apt-utils is not installed
Warning: Detected unsettled top-level await at file:///repro/[eval1]:1
import extract from "extract-zip"; await extract("chrome.zip", { dir: "/tmp/out-node" }); console.log("extract-zip done")
Seems like extract-zip started extracting, but its async operation got stuck in a state where Node thinks there is nothing left to wait for. And the extraction remains incomplete.
Additional information
When this happens through Puppeteer / @puppeteer/browsers, the install appears to leave a partial extraction. For example, only a few files exist:
/tmp/out-node/chrome-linux64/ABOUT
/tmp/out-node/chrome-linux64/MEIPreload/manifest.json
/tmp/out-node/chrome-linux64/MEIPreload/preloaded_data.pb
/tmp/out-node/chrome-linux64/PrivacySandboxAttestationsPreloaded/manifest.json
/tmp/out-node/chrome-linux64/PrivacySandboxAttestationsPreloaded/privacy-sandbox-attestations.dat
/tmp/out-node/chrome-linux64/WidevineCdm/LICENSE
But the expected executable is missing:
/tmp/out-node/chrome-linux64/chrome
Chrome and Puppeteer versions are all fixed on our side and we didn't change them.
Thanks :)
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
pvdstel commentedon May 22, 2026
Running into something similar here. We run Playwright in a sharded GitLab CI job, and then we have a final job that merges the different outputs (which are zip files) together. For this process, Playwright attempts to unzip the files, but simply stops, leaving no reports.
Job output in
node:24.15.0-alpine(digestnode@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f):Job output in
node:24.16.0-alpine(digestnode@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14):(There is no further output from this command.) It appears that the Playwright report merge process simply exits once it attempts to unzip anything.
Edit: seems that updating Playwright to v1.60 fixed this, however.
robjackstewart commentedon May 22, 2026
Can confirm that I am also experiencing this with cypress when running
npx cypress installto install the binary. The process actually exists earlier than it should with a zero exit code.MikeMcC399 commentedon May 22, 2026
MikeMcC399 commentedon May 22, 2026
extract-zip@2.0.1 (current
latest), released Jun 10, 2020 and depends on "yauzl": "^2.10.0"There is a related issue max-mapper/extract-zip#154
https://github.com/thejoshwolfe/yauzl#change-history shows a fix in yauzl@3.3.1
MikeMcC399 commentedon May 22, 2026
It doesn't look like the issue max-mapper/extract-zip#154 is getting any attention, and the latest release of
extract-zipis already 6 years old.fix(install): use native unzip on Linux to avoid yauzl hang
Bump @playwright/test 1.59.1 → 1.60.0 to fix Node 24.16 extract hang
chore(release): 4.4.0 [skip ci]
MikeMcC399 commentedon May 27, 2026
I also encountered this issue using @puppeteer/browsers@2
I updated to @puppeteer/browsers@3.0.4 for the latest version and this does not rely on the 6 year old
extract-zip, so it works with Node.js 24.16.0ci: pin Node 24.15 to fix Playwright install hang
quantizor commentedon May 28, 2026
Adding a Windows Server 2022 + electron-forge data point: same symptom on this side. With
@electron/packagerextracting the Electron template zip, the forge make process exits cleanly with code 0 during extraction, no throw and no error, output silently truncated, so the packaged app is broken with no diagnostic at all.nodejs-lts24.15.0 fine, 24.16.0 broken, no other change in the build. (I dug into the mechanism in a follow-up comment below: it's a backpressure-resume stall on a large entry, and the clean exit is just the event loop draining once the stall leaves nothing ref'd.)For the
@electron/packagerslice specifically, the extraction call chain is@electron/packager/dist/unzip.js→extract-zip@2.0.1→yauzl@2.x, same chain @MikeMcC399 documented above. So electron-forge / electron-builder users are caught on the same bus.Pinning Node back to 24.15.0 is a clean workaround for builds stuck between the unmaintained
extract-zip(max-mapper/extract-zip#154 has had no movement) and the patch bump, while downstream tools migrate toyauzl@3.3.1+.Worth cross-linking #63581 (closed "not planned"); the electron-forge variant probably belongs in this thread.
quantizor commentedon May 28, 2026
A bit more characterization, with a self-contained repro (no browser binary to download) in case it helps.
Minimal repro: extract-zip@2.0.1 (or yauzl@2 directly) extracting a zip whose entries include at least one large, deflated file. On 24.16.0 it stalls; on 24.15.0 it completes. The trigger is per-entry size, i.e. backpressure, not total size or duration:
A single entry large enough to keep the destination write stream above its highWaterMark pauses the read side, and on 24.16.0 it is never resumed. It is the classic-stream backpressure-resume path, not
pipeline()/finished()machinery: a plainreadStream.pipe(writeStream)waiting on'finish'stalls just the same. Instrumenting yauzl's fd-slicer shows the read side actually pushes every byte (identical push sequence on both versions); the large entry's pipe writes nearly all the data and then the terminal event never fires, so the consumer's promise never settles.That last part is why this surfaces three different ways, all the same underlying stall:
Detected unsettled top-level await, exit 13.main()not top-level-awaited, nothing else ref'd: process exits cleanly (exit 0), no warning, output silently truncated. This is what bites electron-forge / electron-builder (the Electron template zip extracts to a partial file, then exit 0, so the packaged app is broken with no error at all). The silent-truncation variant seems the most dangerous.One caveat for anyone trying to minimize it further: it is timing/data-sensitive. I could reproduce it reliably with real yauzl over real zip data, but faithful synthetic reconstructions of the same pipeline (a custom Readable with fd-slicer's pend-serialized reads,
createInflateRaw, anAssertByteCountStream-style passthrough, both.pipeandpipelinetails, modern and old-style streams) all complete on both versions. PlaincreateReadStream -> createWriteStreamandcreateReadStream -> createInflateRaw -> writealso complete. So it looks like a backpressure-resume race that the real deflate-block cadence of a large entry hits and synthetic random data tends to miss, which would explain why it reproduces on some archives/tools and not others.Suspect range is the classic-stream pause/resume changes in
v24.15.0..v24.16.0(theOutgoingMessage'drain'change is HTTP-only, so not this). I have not bisected to a single commit; happy to share the repro scripts if a bisection vehicle is useful.Workaround: pin Node 24.15.0, or move consumers to yauzl@3.3.1+.
MikeMcC399 commentedon May 28, 2026
@robjackstewart
This is fixed in cypress@15.16.0
chore(deps): bump @playwright/test to ^1.60.0
fix(e2e): bump Playwright to 1.60.0 to unblock Node 24.16.0 install
50 remaining items
MikeMcC399 commentedon Jun 10, 2026
I also successfully tested the revert PR against the unfixed cypress@15.15.0 release (see #63834 (comment) for details).
sxa commentedon Jun 10, 2026
We should also consider whether we want to leave this "broken" in the new semver major version (26) or look at fixing it there (either with this revert or another solution).
Renegade334 commentedon Jun 10, 2026
AFAIA, this exposed a genuine latent stream handling bug in older versions of yauzl which has now been patched, but the abandoned extract-zip is using an unpatched version. Reverting on LTS is appropriate given the ecosystem impact, but given that we didn't get any issue reports when this landed on v26.x, there's probably no pressing need to revert it there.
Pin Node 24.15.0 in CI to avoid browser-install hang on 24.16.0
[chore] Pin Node version in CI workflows (#42)
Extract packages with tar instead of extract-zip: nodejs/node#63487
ci: pin Node 24.15.0 to avoid extract-zip regression in 24.16.0
locknut commentedon Jun 12, 2026
Hey, we were impacted by this too and it was very frustrating to see similar issues raised over the past 2 weeks (by others) but closed because maintainers couldn't reproduce it. So we ran this issue through Fable 5 and it came up with the following diagnosis and suggestions. Shamelessly sharing it here in case it's useful to prevent recurrence ... apparently this exact same bug bit in 2018, but tests were never written, so here we are again...
It independently confirmed the bisect result: building v24.16.0 with only 29b1966 reverted makes the hang disappear, so #63834 will fix this on 24.x.
Mechanism: fd-slicer (under yauzl -> extract-zip) ends a slice with the legacy setter idiom
self.destroyed = true; self.push(null)(flag only, no teardown) so the EOF (and sometimes data) is queued behind the destroyed flag. If the pipe destination is applying backpressure at that moment, the only thing that ever restarts delivery is pipe's own drain handler callingsrc.resume()(pipeOnDrainFunctionResult, readable.js:1093). After #62557 thatresume()no-ops on the destroyed flag, so the queued tail and'end'are stranded:dest.end()never runs,'finish'never fires, and awaiting code either wedges or the process exits 0 with the extraction silently truncated.Dependency-free repro (deterministic, ie no Docker, npm, or zip fixture; might be useful as a regression test alongside the revert, which currently only removes the old test):
Why it's environment-dependent (containers/CI and Windows, rarely bare-metal Linux): the backpressure window at end-of-slice depends on libuv's fs completion path. With io_uring active the window happens to stay closed; without it (Windows; most containers,
io_uring_setupcommonly fails under default Docker limits) threadpool completion timing opens it. On bare-metal Linux, UV_USE_IO_URING=0 alone makes the extract-zip case fail 5/5 on 24.16.0 vs 0/5 on 24.15.0.Re the open question about v26: if #62557's contract is ever re-landed, a narrower guard avoids this class: no-op
pause()/resume()only when nothing is deliverable, i.e.state.length === 0 && !(state.ended && !state.endEmitted). Note the pure-wedge variant strands with zero buffered bytes (queued EOF only), so astate.length > 0carve-out alone wouldn't be enough.drbarq commentedon Jun 13, 2026
Tested the proposed narrower guard. Rebuilt v24.16.0 from source with
in both
resume()andpause(). Results:test-stream-destroy.js) passes: a plaindestroy()has no pending EOF, so the no-op contract holds where it was intendedparallel/test-stream*tests pass, zero failuresFINISH received=128If #62557 re-lands on main, this guard looks viable as written.
For anyone bitten right now who can't pin Node: a fixed fd-slicer is at andrewrk/node-fd-slicer#8 (suite 19/19; yauzl@2.10.0's full suite green on 24.16.0). Since upstream fd-slicer is unmaintained, an npm override works today:
Precedent for the re-land question: Node hit this pattern before and chose compat. The legacy-destroy shim in #29176 (12.10, 2019) papered over this same yauzl/fd-slicer
destroy()class after thejoshwolfe/yauzl#110, and that shim is why this lay dormant until now. Fixes were proposed upstream three times and never merged (andrewrk/node-fd-slicer#5, thejoshwolfe/yauzl#123, thejoshwolfe/yauzl#141). Whatever ships here has to assume fd-slicer stays broken, and 26.x is scheduled to become LTS in October with the change still on main.One more datapoint for the io_uring analysis: macOS (no io_uring) reproduces on every run, and the threshold is byte-exact at the default highWaterMark: 65,535 bytes compressed extracts, 65,536 strands.
yktoo commentedon Jun 13, 2026
Took me a whole lot of time to figure out that was the reason
electron-packagerdidn't produce anything. Downgrading to Node 24.15.0 solved the problem for now.The output was ending with:
electron-packager Creating /tmp/electron-packager/linux-x64-template-ykEiWw +125ms electron-packager Extracting /home/dk/.cache/electron/ea3ffe2d5fb91313915c820d8aa37a2601d53095b9f8a841d9937bb2eff8264f/electron-v41.7.2-linux-x64.zip to /tmp/electron-packager/linux-x64-template-ykEiWw +0ms extract-zip creating target directory /tmp/electron-packager/linux-x64-template-ykEiWw +0ms extract-zip opening /home/dk/.cache/electron/ea3ffe2d5fb91313915c820d8aa37a2601d53095b9f8a841d9937bb2eff8264f/electron-v41.7.2-linux-x64.zip with opts { dir: '/tmp/electron-packager/linux-x64-template-ykEiWw' } +0ms extract-zip zipfile entry version +1ms extract-zip extracting entry { filename: 'version', isDir: false, isSymlink: false } +0ms extract-zip mkdir { dir: '/tmp/electron-packager/linux-x64-template-ykEiWw', recursive: true } +0ms extract-zip opening read stream /tmp/electron-packager/linux-x64-template-ykEiWw/version +0ms extract-zip finished processing version +3ms extract-zip zipfile entry locales/ja.pak +0ms extract-zip extracting entry { filename: 'locales/ja.pak', isDir: false, isSymlink: false } +0ms extract-zip mkdir { dir: '/tmp/electron-packager/linux-x64-template-ykEiWw/locales', recursive: true } +0ms extract-zip opening read stream /tmp/electron-packager/linux-x64-template-ykEiWw/locales/ja.pak +0msMikeMcC399 commentedon Jun 13, 2026
@yktoo
You didn't say which version of
electron-packager, you're using, but it looks like it's fixed in @electron/packager@20.0.1yktoo commentedon Jun 14, 2026
@MikeMcC399 ah shoot, thanks for pointing that out! I was still using the discontinued
electron-packagerpackage. Migrating to@electron/packagerindeed solves the problem also for Node 24.16.x.omBratteng commentedon Jun 25, 2026
Node 24.18.0 was recently released, and running the script above with the new version seems to pass.
sxa commentedon Jun 25, 2026
@omBratteng Thanks for confirming - I had also checked it on the release build on Linux/x64 before it was published so I'll close this now. Hopefully everyone above can now undo anything they've done to pin. I was going to tag everyone who had referenced this issue but there are a lot so hopefully they'll notice that this has been fixed now.
Note that this is a fix specific to v24 and is likely to continue to fail in future major versions of Node as per the earlier comment.
omBratteng commentedon Jun 26, 2026
Yeah, I can confirm that it still fails on latest v26 version