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

Firebase云函数Express应用上传文件遇Unexpected end of form错误

Firebase云函数Express应用文件上传报“Unexpected end of form”错误的解决方法

问题描述

我在Firebase云函数的Express应用中开发文件上传路由,先后尝试了formidable、multer和busboy模块,但在Firebase模拟器运行时,无论通过前端还是Postman发起请求,都会抛出**“Unexpected end of form at Multipart._final”**错误。但将相同代码部署到独立Express应用时却能正常运行。以下是基于busboy编写的最终代码:

const express = require("express");
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const mammoth = require("mammoth")
const Busboy = require("busboy")

admin.initializeApp(functions.config().firebase);

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/document/placeholders", async (req, res) => {

try {
    
  const busboy = Busboy({ headers: req.headers });
  busboy.on('file', async (fieldname, file, filename, encoding, mimetype) => {
    const chunks = [];
    file.on('data', (chunk) => chunks.push(chunk));
    file.on('end', async () => {
      const buffer = Buffer.concat(chunks);
      console.log(filename);
      const { value: text } = await mammoth.extractRawText({ buffer });
      console.log('Extracted text:', text);
      res.status(200).send('File received');
    });
  });

  busboy.on('error', (err) => {
    console.error('Error:', err);
    res.status(500).send('Error uploading file');
  });

  req.pipe(busboy)
} catch (error) {
   return res.json({success: false, error: error.message || error})
}
});

问题原因

这个错误的核心是全局注册的express.json()和express.urlencoded()中间件提前解析了请求体,导致busboy无法读取完整的multipart/form-data数据流。Firebase云函数的请求处理逻辑和独立Express存在差异,这些中间件会优先消费请求体,当busboy尝试通过req.pipe()读取时,数据流已经被截断,从而触发“Unexpected end of form”错误。

另外,原代码中存在重复发送响应的风险(比如文件处理完成后发送响应,但busboy的error或finish事件可能再次触发响应),这也可能导致请求状态异常。

解决方案

  1. 隔离解析中间件:不要全局注册express.json()和express.urlencoded(),仅在需要处理JSON或表单数据的路由上使用。
  2. 完善busboy事件处理:添加响应标记避免重复发送,同时监听finish事件处理无文件上传的情况。
  3. 确保请求体完整传递:让busboy直接处理原始请求流,不经过其他解析中间件。

修正后的代码:

const express = require("express");
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const mammoth = require("mammoth");
const Busboy = require("busboy");
const cors = require("cors");

admin.initializeApp(functions.config().firebase);

const app = express();
// 仅全局启用cors,不全局解析请求体
app.use(cors());

// 为需要处理JSON/表单数据的路由单独注册解析中间件
const apiRouter = express.Router();
apiRouter.use(express.json());
apiRouter.use(express.urlencoded({ extended: true }));
// 示例:其他需要解析的路由
apiRouter.post("/other-endpoint", (req, res) => {
  res.json({ data: req.body });
});
app.use("/api", apiRouter);

app.post("/document/placeholders", async (req, res) => {
  let responseSent = false;

  try {
    const busboy = Busboy({ headers: req.headers });

    busboy.on("file", async (fieldname, file, filename, encoding, mimetype) => {
      const chunks = [];
      file.on("data", (chunk) => chunks.push(chunk));
      file.on("end", async () => {
        if (responseSent) return;
        try {
          const buffer = Buffer.concat(chunks);
          console.log(filename);
          const { value: text } = await mammoth.extractRawText({ buffer });
          console.log("Extracted text:", text);
          responseSent = true;
          res.status(200).send("File received");
        } catch (extractErr) {
          responseSent = true;
          res.status(500).send(`文本提取失败:${extractErr.message}`);
        }
      });
    });

    busboy.on("error", (err) => {
      if (responseSent) return;
      responseSent = true;
      console.error("Busboy错误:", err);
      res.status(500).send("文件上传出错");
    });

    busboy.on("finish", () => {
      if (!responseSent) {
        responseSent = true;
        res.status(400).send("未检测到上传文件");
      }
    });

    req.pipe(busboy);
  } catch (error) {
    if (!responseSent) {
      responseSent = true;
      res.status(500).json({ success: false, error: error.message || error });
    }
  }
});

// 导出云函数
exports.api = functions.https.onRequest(app);

额外注意事项

  • 请求时务必确保Content-Type设置为multipart/form-data,不要手动指定Content-Length,由客户端自动计算。
  • 修改代码后重启Firebase模拟器,避免缓存导致的旧代码执行。
  • 若使用multer,同样需要将其路由与解析中间件隔离,比如用单独的Router挂载multer路由,不应用json/urlencoded中间件。

内容的提问来源于stack exchange,提问作者Silvano H.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 11:05:11