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的网络发送队列,不会等待客户端完整接收所有数据后再执行后续代码:
- 如果在
res.send()后继续写设置响应头、发送其他响应的逻辑,就会出现响应头发送完成后重复设头的报错 - 如果在
res.send()前执行删除逻辑,若zip生成逻辑依赖源文件夹的磁盘文件读取,就会因为文件被提前删除导致生成的zip为空 - 如果给
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
相关产品推荐
相关产品推荐

