Skip to content

Commit 2af013b

Browse files
sunnylqmclaude
andcommitted
fix(zip): handle writePromise rejection from the start
A write-stream error (ENOSPC, EACCES, output path is a directory) can come while diff or RepackSink is still building entries, long before anyone awaits writePromise; with no handler attached yet it surfaced as an unhandled rejection and ended the process before abort() could clean up. The no-op handler now goes on at creation; awaiting the promise still rejects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent aa4e611 commit 2af013b

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

‎src/utils/zip-entries.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,12 @@ export function createOutputZip(output: string) {
159159
resolve(void 0);
160160
});
161161
});
162+
// a write error can come while the caller is still building entries, long
163+
// before it awaits writePromise: mark it handled now (awaiting the original
164+
// promise still rejects) so it never surfaces as an unhandled rejection
165+
writePromise.catch(() => {});
162166
// 失败时释放输出 fd 并删掉半成品,避免残留一个截断的包
163167
const abort = async () => {
164-
writePromise.catch(() => {});
165168
zipfile.outputStream.unpipe(writeStream);
166169
// yazl 的 outputStream 类型声明缺 destroy,运行时是 PassThrough
167170
(zipfile.outputStream as unknown as { destroy(): void }).destroy();

‎tests/zip-entries.test.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import os from 'os';
44
import path from 'path';
55
import { ZipFile } from 'yazl';
66
import { removeSync } from '../src/utils/fs-helpers';
7-
import { enumZipEntries } from '../src/utils/zip-entries';
7+
import { createOutputZip, enumZipEntries } from '../src/utils/zip-entries';
88

99
function zipBuffer(entries: Record<string, Buffer>): Promise<Buffer> {
1010
const zip = new ZipFile();
@@ -81,3 +81,26 @@ describe('enumZipEntries', () => {
8181
expect(nestedTempDirs()).toEqual(before);
8282
});
8383
});
84+
85+
describe('createOutputZip', () => {
86+
test('a write error before writePromise is awaited is not an unhandled rejection', async () => {
87+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rnu-output-zip-'));
88+
const unhandled: unknown[] = [];
89+
const onUnhandled = (reason: unknown) => unhandled.push(reason);
90+
process.on('unhandledRejection', onUnhandled);
91+
try {
92+
// a directory where the zip should go: the write stream fails at once,
93+
// while the caller would still be adding entries
94+
const output = path.join(dir, 'out.ppk');
95+
fs.mkdirSync(output);
96+
const { writePromise, abort } = createOutputZip(output);
97+
await new Promise((resolve) => setTimeout(resolve, 50));
98+
expect(unhandled).toEqual([]);
99+
await expect(writePromise).rejects.toMatchObject({ code: 'EISDIR' });
100+
await abort();
101+
} finally {
102+
process.off('unhandledRejection', onUnhandled);
103+
removeSync(dir);
104+
}
105+
});
106+
});

0 commit comments

Comments
 (0)