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

Next.js API路由返回CSV仅含表头的问题排查求助

问题描述

我理解的文件下载流程是前端调用API路由,其余操作在服务器端完成(若有误请指正)。我的任务是从Firestore读取数据并写入CSV文件,本地生成的文件数据完整,但前端下载后仅包含name和email表头。以下是我的API路由代码:

import { NextApiHandler } from "next";
import fs from "fs";
import { stringify } from "csv-stringify";
import { firestore } from "../../firestore";
import { unstable_getServerSession } from "next-auth/next";
import { authOptions } from "./auth/[...nextauth]";

const exportFromFirestoreHandler: NextApiHandler = async (req, res) => {
  const session = await unstable_getServerSession(req, res, authOptions);

  if (!session) {
    return res.status(401).json({ message: "You must be authorized here" });
  }

  const filename = "guestlist.csv";
  const writableStream = fs.createWriteStream(filename);
  const columns = ["name", "email"];
  const stringifier = stringify({ header: true, columns });

  const querySnapshot = await firestore.collection("paprockibrzozowski").get();
  await querySnapshot.docs.forEach((entry) => {
    stringifier.write([entry.data().name, entry.data().email], "utf-8");
  });
  stringifier.pipe(writableStream);
  const csvFile = await fs.promises.readFile(
    `${process.cwd()}/${filename}`,
    "utf-8"
  );

  res.status(200).setHeader("Content-Type", "text/csv").send(csvFile);
};

export default exportFromFirestoreHandler;
问题原因与解决方案

核心问题是流操作的异步未等待完成:你在触发CSV写入流后立刻读取文件,但此时数据还没完全写入磁盘,所以前端只能拿到提前生成的表头。

错误点拆解

  1. querySnapshot.docs.forEach是同步遍历,虽然querySnapshot已await,但stringifier.write是异步流操作,forEach不会等待所有数据写入完成。
  2. stringifier.pipe(writableStream)后直接执行readFile,此时文件写入还未结束,读到的内容不完整。

最优修正方案(直接流式返回,无需本地文件)

不需要生成本地文件,直接将CSV流通过响应对象返回给前端,既节省磁盘IO,又能确保数据完整:

import { NextApiHandler } from "next";
import { stringify } from "csv-stringify";
import { firestore } from "../../firestore";
import { unstable_getServerSession } from "next-auth/next";
import { authOptions } from "./auth/[...nextauth]";

const exportFromFirestoreHandler: NextApiHandler = async (req, res) => {
  const session = await unstable_getServerSession(req, res, authOptions);

  if (!session) {
    return res.status(401).json({ message: "You must be authorized here" });
  }

  const columns = ["name", "email"];
  const stringifier = stringify({ header: true, columns });

  // 设置响应头,告诉前端这是CSV文件
  res.setHeader("Content-Type", "text/csv");
  res.setHeader("Content-Disposition", 'attachment; filename="guestlist.csv"');

  // 将stringifier流直接管道到响应对象
  stringifier.pipe(res);

  const querySnapshot = await firestore.collection("paprockibrzozowski").get();
  querySnapshot.docs.forEach((entry) => {
    stringifier.write([entry.data().name, entry.data().email], "utf-8");
  });

  // 结束流,确保所有数据发送完成
  stringifier.end();
};

export default exportFromFirestoreHandler;

若需保留本地文件的替代方案

如果必须生成本地文件后再返回,需要用Promise包裹流的finish事件,等待写入完成后再读取:

// 替换原代码中pipe和readFile的部分
await new Promise((resolve) => {
  stringifier.pipe(writableStream).on("finish", resolve);
  // 结束stringifier,触发流的finish事件
  stringifier.end();
});

const csvFile = await fs.promises.readFile(`${process.cwd()}/${filename}`, "utf-8");
res.status(200).setHeader("Content-Type", "text/csv").send(csvFile);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 01:48:19