You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js tar包流write方法未调用回调致程序挂起问题排查

问题:Express处理Tar包提取时卡住,无法继续执行

代码目标

这段代码的目标是将上传的文件提取到Express应用中并保存其中一个文件,接着解析该yaml格式的文件,解析后的内容将用于调用方法把原始tar包上传至GCP存储桶。

问题现象

当日志打印出will write pubspec后,程序就不再继续执行,甚至调试时无法暂停。这里的Pubspec指的是要写入的yaml文件pubspec.yaml,使用的是tar包。

代码示例

app.post("/api/packages/upload", authenticate, versioning, async (req: express.Request, res: express.Response) => {
    const files = req.files;
    if (!files) {
        return res.json({
            "error": {
                "code": "400",
                "message": "No files were uploaded."
            }
        })
    }
    Object.keys(files!).forEach(async (key) => {
        const uploadedFile: UploadedFile = files![key] as UploadedFile;
        await fs.mkdir("./tmp/pubspecs", {
            recursive: true
        });
        const tarStream = tar.extract({
            cwd: "./tmp/pubspecs"
            }, ["pubspec.yaml"])

        const data = await fs.readFile(uploadedFile.tempFilePath);
        await new Promise((resolve, reject) => {
            console.log("Will write pubspec");
            tarStream.write(data, (err) => {
                if (err) {
                    console.log(err);
                    reject(err);
                }
                console.log("wrote pubspec");
                resolve(() => {});
            });
        });
        console.log("Will read yaml");
        const pubspec = YAML.parse((await fs.readFile("./tmp/pubspecs/pubspec.yaml")).toString());
        console.log("read yaml");
        const name = pubspec.name;
        const version = pubspec.version;
        console.log("name: " + name);
        await uploadPackage(name, PackageType.DART, version, uploadedFile);
        res.status(204);
        res.location(`${process.env.URL_BASE}/dart/${name}/${version}/package.tar.gz`)
        await fs.rm(".tmp/pubspecs/pubspec.yaml");
        res.send();
    });
});

问题原因与修复方案

核心问题

  1. Stream未结束导致Promise挂起:tarStream.write()仅完成数据写入缓冲区,并未触发提取操作的结束逻辑,Stream一直处于打开状态,Promise无法resolve,程序卡住。
  2. forEach异步处理冲突:forEach中的async函数不会阻塞循环,多文件上传时会出现竞态条件,还可能重复发送响应。
  3. 文件路径不一致:删除临时文件时使用.tmp/pubspecs/pubspec.yaml,但之前创建的目录是./tmp/pubspecs,路径不匹配导致删除失败。

修复后的代码

app.post("/api/packages/upload", authenticate, versioning, async (req: express.Request, res: express.Response) => {
    const files = req.files;
    if (!files) {
        return res.json({
            "error": {
                "code": "400",
                "message": "No files were uploaded."
            }
        });
    }

    // 用for...of替代forEach,确保异步操作顺序执行
    for (const key of Object.keys(files!)) {
        const uploadedFile: UploadedFile = files![key] as UploadedFile;
        const tmpDir = "./tmp/pubspecs";
        await fs.mkdir(tmpDir, { recursive: true });
        
        const tarStream = tar.extract({
            cwd: tmpDir
        }, ["pubspec.yaml"]);

        const data = await fs.readFile(uploadedFile.tempFilePath);
        
        await new Promise((resolve, reject) => {
            console.log("Will write pubspec");
            // 监听finish事件确认提取完成
            tarStream.on("finish", () => {
                console.log("Extraction finished");
                resolve(null);
            });
            tarStream.on("error", (err) => {
                console.error("Tar extraction error:", err);
                reject(err);
            });
            
            // 写入数据后结束Stream,触发finish事件
            tarStream.write(data, (err) => {
                if (err) {
                    console.error("Write error:", err);
                    reject(err);
                }
                console.log("Wrote pubspec data");
                tarStream.end();
            });
        });

        console.log("Will read yaml");
        const pubspecPath = `${tmpDir}/pubspec.yaml`;
        const pubspecContent = await fs.readFile(pubspecPath, "utf8");
        const pubspec = YAML.parse(pubspecContent);
        console.log("Read yaml");
        
        const name = pubspec.name;
        const version = pubspec.version;
        console.log("name: " + name);
        
        await uploadPackage(name, PackageType.DART, version, uploadedFile);
        
        res.status(204);
        res.location(`${process.env.URL_BASE}/dart/${name}/${version}/package.tar.gz`);
        await fs.rm(pubspecPath);
        res.send();
    }
});

关键修复点

  • 监听Stream的finish事件:通过finish事件确认tar包提取操作完全完成,确保Promise能正常resolve。
  • 主动结束Stream:调用tarStream.end()关闭流,触发finish事件,解决程序卡住的问题。
  • 替换forEach为for...of:保证异步操作按顺序执行,避免多文件处理时的竞态条件和重复响应。
  • 统一文件路径:修正删除临时文件的路径,确保操作有效。

内容的提问来源于stack exchange,提问作者Jacob Miller

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 13:15:24