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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 00:18:19