[Bug]: @puppeteer/browsers: Abruptly truncated downloads exit with code 0 instead of failing
Steps to reproduce
We use @puppeteer/browsers in the emscripten CI to install browsers for testing: https://github.com/emscripten-core/emscripten/blob/b6f4364e625d07c497db8ecb9cb29b8f97f35cee/.circleci/config.yml#L46
Occasionally we have seen flakes that have strange output that looks like this:
Downloading firefox nightly - 99.4 MB [ ] 0% 0.0s
browser installed to:
/bin/bash: line 4: : command not found
Exited with code exit status 127 Description
The AI analysis of what is happening looks correct to me:
When a browser download is prematurely closed or truncated by the
server/network (e.g., connection reset or dropped socket midway through
download), @puppeteer/browsers install exits with code 0 instead of failing
with an error.
This happens particularly for browsers that do not have an expectedHash (such
as Firefox Nightly), leading to silent failures in CI scripts that expect either
a valid executable on stdout or a non-zero exit code.
Environment
- Package:
@puppeteer/browsers - Version:
3.2.3(and latest) - Node:
>=18 - OS: Linux / macOS
Steps to Reproduce
Run the following minimal Node.js script that simulates a server terminating the
download stream early after sending a Content-Length header:
import http from 'node:http';
import { spawn } from 'node:child_process';
const server = http.createServer((req, res) => {
if (req.url.includes('firefox_versions.json')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ FIREFOX_NIGHTLY: '158.0a1' }));
return;
}
// Simulate download: announce 100 MB, send 1 KB, then destroy socket abruptly
res.writeHead(200, {
'Content-Length': '104857600',
'Content-Type': 'application/x-tar',
});
res.write(Buffer.alloc(1024));
setTimeout(() => req.socket.destroy(), 50);
});
server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
const cp = spawn('npx', [
'@puppeteer/browsers',
'install',
'firefox@nightly',
'--base-url',
`http://127.0.0.1:${port}`,
'--format={{path}}',
]);
cp.stdout.on('data', d => console.log('STDOUT:', d.toString()));
cp.stderr.on('data', d => console.error('STDERR:', d.toString()));
cp.on('close', code => {
console.log(`Process exited with code: ${code}`);
server.close();
});
});Root Cause Analysis
There are two issues compounding this:
packages/browsers/src/httpUtil.ts(downloadFile):const file = createWriteStream(destinationPath); file.on('close', () => { if (verifier && expectedHash) { try { verifier.verify(url, destinationPath, expectedHash); } catch (err) { reject(err); return; } } return resolve(); });When
expectedHashis not supplied,file.on('close')unconditionally callsresolve(). When the socket is closed prematurely,response.pipe(file)ends the write stream and emits'close'. It never verifies whetherdownloadedBytes === totalBytesagainst theContent-Lengthheader (if available), nor is there an error handler attached toresponse(response.on('error')).packages/browsers/src/fileUtil.ts(extractTar):const unpack = spawn(internalConstantsForTesting[decompressUtilityName], ['-d'], { stdio: ['pipe', 'pipe', 'inherit'], }) .once('error', handleError(decompressUtilityName)) .once('exit', code => { logger?.(DEBUG_PREFIXES.fileUtil)?.(`${decompressUtilityName} exited, code=${code}`); });If the truncated archive is passed to
extractTar(),xz -dorbzip2 -dexits with a non-zero status (e.g., code 2:File format not recognized), but the.once('exit')handler only logs this to the debug logger and does not reject the promise.modern-tarreaches EOF having unpacked 0 files, emits'finish', and fulfills the promise as successful.
Expected results
The download should detect that the stream ended prematurely
(downloadedBytes < totalBytes), reject the promise, clean up the
corrupted/partial download, and exit with a non-zero status code (e.g. exit
code 1) with an error on stderr.
Actual results
npx @puppeteer/browsers installprints the progress bar up to the point of disconnection and then exits with code 0.- Nothing is printed to
stdout(--format={{path}}produces no output). - Any downstream script or pipeline expecting an executable path or a non-zero failure code fails unexpectedly.
Source: puppeteer/puppeteer