Node.js服务多次请求下载CSV文件返回空文件问题排查
问题根因
核心错误是csvStream被定义在了全局作用域,被所有请求复用:
- Node.js中的流是一次性使用资源,第一次请求处理完成时调用
csvStream.end()后,该流就进入终止状态,无法再执行写入、管道绑定操作 - 后续请求到来时,虽然你仍然执行
csvStream.pipe(res)尝试把流绑定到新的响应对象,但已经终止的流不会输出任何有效数据,只会传递结束状态,因此客户端只能收到响应头,下载到空文件 - 服务器重启后全局的
csvStream会被重新初始化,因此第一次请求可以正常工作,和你描述的复现规律完全一致。
另外代码里还有两处不规范的点:
- 返回CSV文件时
Content-Type设置错误,text/html是HTML页面的MIME类型,CSV对应的正确类型是text/csv - 没有对流错误、数据库查询错误做捕获处理,出现异常时容易导致请求挂起、服务抛出未捕获错误。
修复代码
将CSV流的初始化移动到每次请求的处理逻辑内部,保证每个请求使用独立的流实例,同时修正响应头、补全错误捕获即可:
const { format } = require("@fast-csv/format"); const router = express.Router(); router.route("/:collection/csv").get(requireModel, createCsv); const csvFromDatabase = (data, res) => { // 每次请求创建全新的CSV流实例,禁止全局复用 const csvStream = format({ delimiter: ";", headers: true }); // 绑定流错误监听 csvStream.on("error", () => { res.status(500).end("CSV文件生成失败"); }); csvStream.pipe(res); data.forEach((row) => { const { _id, __v, ...newRow } = row._doc; csvStream.write(newRow); }); csvStream.end(); }; const createCsv = async (req, res, next) => { try { const { model } = req; const items = await model.find({}); // 设置正确的CSV响应头 res.setHeader("Content-disposition", "attachment; filename=file.csv"); res.setHeader("Content-type", "text/csv; charset=UTF-8"); csvFromDatabase(items, res); } catch (err) { next(err); } };
大数据量场景优化
如果单表数据量超过1万条,不建议直接用model.find({})一次性把所有数据加载到内存,可以配合Mongoose的查询游标逐行读取、逐行写入CSV,大幅降低服务内存占用:
const createCsv = async (req, res, next) => { try { const { model } = req; res.setHeader("Content-disposition", "attachment; filename=file.csv"); res.setHeader("Content-type", "text/csv; charset=UTF-8"); const csvStream = format({ delimiter: ";", headers: true }); csvStream.on("error", () => res.status(500).end("CSV文件生成失败")); csvStream.pipe(res); // 使用游标逐行读取数据库数据,不一次性加载全部内容到内存 const cursor = model.find({}).cursor(); for await (const doc of cursor) { const { _id, __v, ...newRow } = doc._doc; csvStream.write(newRow); } csvStream.end(); } catch (err) { next(err); } };
内容的提问来源于stack exchange,提问作者Manuel
相关产品推荐
相关产品推荐

