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

Express发送响应后如何删除zip文件及临时文件夹

问题说明

需求是在Express接口完成客户端下载响应后,自动删除服务器上生成zip对应的源文件夹,现有实现存在两类报错:

  • 若res.send(data)前不加return,会抛出Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client错误
  • 若把return res.send(data)移到目录删除逻辑之后,会因为目录被提前删除,导致客户端下载到的zip包为空
问题根因

res.send()属于异步非阻塞方法,调用后仅会把响应内容加入Node.js的网络发送队列,不会等待客户端完整接收所有数据后再执行后续代码:

  1. 如果在res.send()后继续写设置响应头、发送其他响应的逻辑,就会出现响应头发送完成后重复设头的报错
  2. 如果在res.send()前执行删除逻辑,若zip生成逻辑依赖源文件夹的磁盘文件读取,就会因为文件被提前删除导致生成的zip为空
  3. 如果给res.send()加return,后面的删除逻辑会直接被截断,永远不会执行
可靠实现方案

核心逻辑是 监听响应生命周期的结束事件,等响应完全发送完成(或中途异常断开)后再执行目录删除,不要直接把删除逻辑写在res.send()的前后。

方案1:内存buffer版(适合小文件下载)

如果zip体积不大,可以继续用buffer的方式生成zip,把删除逻辑绑定到响应的finish和close事件上,保证无论下载成功还是中途失败,都会清理源目录:

const fse = require('fs-extra');
const admz = require('adm-zip');

exports.Download = async (req, res) => {
    // 注意:folder变量请从req参数合法获取,必须做路径遍历校验,禁止直接拼接用户传入的未校验参数
    const targetDir = `./download/${folder}`;
    try {
        const zp = new admz();
        zp.addLocalFolder(targetDir);
        const downloadFileName = 'downloaded.zip';
        const zipBuffer = zp.toBuffer();

        res.set('Content-Type', 'application/octet-stream');
        res.set('Content-Disposition', `attachment; filename=${downloadFileName}`);
        res.set('Content-Length', zipBuffer.length);

        // 定义统一清理函数
        const cleanSourceDir = () => {
            // 移除事件监听,防止重复触发
            res.off('finish', cleanSourceDir);
            res.off('close', cleanSourceDir);
            if (fse.existsSync(targetDir)) {
                // force: true 忽略文件不存在等异常,避免删文件报错影响进程
                fse.rmSync(targetDir, { recursive: true, force: true });
            }
        };

        // 响应正常发送完成触发
        res.on('finish', cleanSourceDir);
        // 响应中途断开(用户取消、网络异常)触发,避免遗留垃圾文件
        res.on('close', cleanSourceDir);

        // 不需要加return,send后不会再执行其他响应逻辑
        res.send(zipBuffer);
    } catch (err) {
        console.log('下载接口异常:', err);
        // 接口报错时也要先清理目录,避免留垃圾文件
        if (fse.existsSync(targetDir)) {
            fse.rmSync(targetDir, { recursive: true, force: true });
        }
        return res.status(404).render('pages/404');
    }
}

方案2:流式传输版(适合大文件下载,内存占用低)

如果zip体积较大,把整个zip读进内存容易造成服务OOM,可以先把zip写到临时磁盘路径,用Express自带的res.download()做流式传输,传输完成后统一删除整个目录:

const fse = require('fs-extra');
const admz = require('adm-zip');
const path = require('path');

exports.Download = async (req, res) => {
    const targetDir = `./download/${folder}`;
    const tempZipPath = path.join(targetDir, 'temp_download.zip');
    try {
        const zp = new admz();
        zp.addLocalFolder(targetDir);
        // 直接把zip写入磁盘,不占用大量服务内存
        zp.writeZip(tempZipPath);

        const cleanAllFile = () => {
            res.off('finish', cleanAllFile);
            res.off('close', cleanAllFile);
            if (fse.existsSync(targetDir)) {
                fse.rmSync(targetDir, { recursive: true, force: true });
            }
        };

        res.on('finish', cleanAllFile);
        res.on('close', cleanAllFile);

        // 流式发送zip文件,内存占用稳定在几KB级别
        res.download(tempZipPath, 'downloaded.zip', (transferErr) => {
            if (transferErr) {
                console.log('文件传输异常:', transferErr);
            }
        });
    } catch (err) {
        console.log('下载接口异常:', err);
        if (fse.existsSync(targetDir)) {
            fse.rmSync(targetDir, { recursive: true, force: true });
        }
        return res.status(404).render('pages/404');
    }
}
注意事项
  • 禁止直接把删除逻辑写在res.send()之后,res.send不会阻塞等待传输完成,必然会出现文件提前删除或响应头重复设置的问题
  • 必须同时监听close事件,否则用户中途取消下载时,源目录会永久残留在服务器上,占用磁盘空间
  • 拼接目录路径的folder参数必须做合法性校验,过滤../、./等跨路径字符,防止路径遍历漏洞导致服务器其他文件被误删
  • 删除文件时加force: true参数,忽略文件被短暂占用、文件不存在等边缘异常,避免删除逻辑报错导致服务崩溃

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 22:09:17