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

2022年Express结合Google Cloud Functions处理multipart/form-data问题求助

Express + Google Cloud Functions 处理multipart/form-data部署后报错问题

2022年10月,基于Express的后端结合Google Cloud Functions处理multipart/form-data请求时遭遇异常:本地运行所有代码均正常,但部署至云端后持续报错。初始报错为「Busboy is not a constructor」,移除new Busboy改为直接调用后,又触发「Unexpected end of form」错误。


后端尝试的代码(本地正常、部署异常)

// // // Processing multipart/form-data files middleware

// // const busboy = require("busboy"); // for parsing multipart/form-data
// // const path = require("path");
// // const fs = require("fs");
// // const asyncHandler = require("express-async-handler");

// // const processFileMiddleware = asyncHandler((req, res, next) => {
// //   const busboyInstance = busboy({ headers: req.headers });

// //   busboyInstance.on("file", async (fieldname, file, filename, encoding, mimetype) => {
// //     console.log(filename);
// //     // this logs the following:

// //     const filepath = "./uploads/" + await filename.filename;
// //     console.log(filepath)
// //     const writeStream = fs.createWriteStream(filepath);

// //     file.pipe(writeStream);

// //     file.on("end", () => {
// //         // console.log("File [" + fieldname + "] Finished");
// //         req.file = { originalFilename: filename, filepath };
// //         next();
// //         }
// //     );
// //   });

// //   busboyInstance.on("finish", () => {
// //     console.log("Done parsing form!");
// //     next();
// //   });

// //   req.pipe(busboyInstance);
// // });

// // module.exports = { processFileMiddleware };

// const path = require('path');
// const os = require('os');
// const fs = require('fs');

// // Node.js doesn't have a built-in multipart/form-data parsing library.
// // Instead, we can use the 'busboy' library from NPM to parse these requests.
// const Busboy = require('busboy');

// const processFileMiddleware = (req, res, next) => {
//   if (req.method !== 'POST') {
//     // Return a "method not allowed" error
//     return res.status(405).end();
//   }

//   console.log('Processing file...');
//   const busboy = Busboy({headers: req.headers});
//   const tmpdir = "./uploads/";

//   // This object will accumulate all the fields, keyed by their name
//   const fields = {};

//   // This object will accumulate all the uploaded files, keyed by their name.
//   const uploads = {};

//   const fileWrites = [];

//   // This code will process each file uploaded.
//   busboy.on('file', (fieldname, file, {filename, encoding}) => {
//     // Note: os.tmpdir() points to an in-memory file system on GCF
//     // Thus, any files in it must fit in the instance's memory.
//     console.log(`Processed file ${filename}`);
//     const filepath = path.join(tmpdir, filename);
//     uploads['file'] = filepath;

//         console.log(filepath)
//     const writeStream = fs.createWriteStream(filepath);
//     file.pipe(writeStream);

//     // File was processed by Busboy; wait for it to be written.
//     // Note: GCF may not persist saved files across invocations.
//     // Persistent files must be kept in other locations
//     // (such as Cloud Storage buckets).
//     const promise = new Promise((resolve, reject) => {
//       file.on('end', () => {
//         writeStream.end();
//       });
//       writeStream.on('finish', resolve);
//       writeStream.on('error', reject);
//     });
//     fileWrites.push(promise);
//   });

//   // Triggered once all uploaded files are processed by Busboy.
//   // We still need to wait for the disk writes (saves) to complete.
//   busboy.on('finish', async () => {
//     await Promise.all(fileWrites);

//     /**
//      * TODO(developer): Process saved files here
//      */
//     for (const file in uploads) {
//       fs.unlinkSync(uploads[file]);
//     }
//     res.send();
//   });

// //   busboy.end(req.body);

// console.log(req.body)

//     // The raw bytes of the upload will be in req.rawBody. Send it to
//     // Busboy, and get a callback when it's finished.
//     busboy.pipe(req.rawBody);

//     res.send();

// process incoming request
// upload the file to the bucket

// const Busboy = require("busboy");
// const path = require("path");
// const os = require("os");
// const fs = require("fs");
// const asyncHandler = require("express-async-handler");
// const { Storage } = require("@google-cloud/storage");
// const { uploadFile } = require("../cloudstorage/uploadFile");
// const random = (() => {
//   const buf = Buffer.alloc(16);
//   return () => randomFileSync(buf).toString("hex");
// })();

// const processFileMiddleware = asyncHandler((req, res, next) => {
//   if (req.method !== "POST") {
//     // Return a "method not allowed" error
//     return res.status(405).end();
//   }

//   console.log("Processing file...");

//   const bb = Busboy({ headers: req.headers });
//   bb.on("file", async (name, file, { filename }) => {
//     const saveTo = path.join(os.tmpdir(), filename);
//     file.pipe(fs.createWriteStream(saveTo));
//     console.log(`File ${filename} saved to ${saveTo}`);

//     try {
//       await uploadFile(file, filename);
//     } catch (err) {
//       console.log(err);
//     }
//   });
//   bb.on("finish", () => {
//     //   res.writeHead(200, { 'Connection': 'close' });
//     next();
//   });

//   // console.log(Object.keys(req), bb);
//  // docs say this is needed

//   req.pipe(bb);

//   req.on("end", () => {
//     console.log("Done parsing form!");
//     next();
//   });
// });

// module.exports = { processFileMiddleware };

const asyncHandler = require("express-async-handler");

const processFileMiddleware = asyncHandler((req, res) => {
  const BusBoy = require("busboy");
  const path = require("path");
  const os = require("os");
  const fs = require("fs");
  const uuid = require("uuid");
  

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


  let imageToBeUploaded = {};
  let imageFileName;
  // String for image token
  let generatedToken = uuid.v4();

  busboy.on("file", (fieldname, file, filename, encoding, mimetype) => {
    console.log(fieldname, file, filename, encoding, mimetype);
    if (mimetype !== "image/jpeg" && mimetype !== "image/png") {
      return res.status(400).json({ error: "Wrong file type submitted" });
    }
    // my.image.png => ['my', 'image', 'png']
    const imageExtension = filename.split(".")[filename.split(".").length - 1];
    // 32756238461724837.png
    imageFileName = `${Math.round(
      Math.random() * 1000000000000
    ).toString()}.${imageExtension}`;
    const filepath = path.join(os.tmpdir(), imageFileName);
    imageToBeUploaded = { filepath, mimetype };
    file.pipe(fs.createWriteStream(filepath));
  });
  busboy.on("finish", () => {
    admin
      .storage()
      .bucket()
      .upload(imageToBeUploaded.filepath, {
        resumable: false,
        metadata: {
          metadata: {
            contentType: imageToBeUploaded.mimetype,
            //Generate token to be appended to imageUrl
            firebaseStorageDownloadTokens: generatedToken,
          },
        },
      });
  });
  busboy.end(req.rawBody);
});

module.exports = { processFileMiddleware };

具体报错信息

node:events:505 throw er;
 // Unhandled 'error' event ^ Error: Unexpected end of form at Multipart._final (functions/node_modules/busboy/lib/types/multipart.js:588:17) at callFinal (node:internal/streams/writable:695:27) at prefinish 
(node:internal/streams/writable:724:7) at finishMaybe 
(node:internal/streams/writable:734:5) at Multipart.Writable.end 
(node:internal/streams/writable:632:5) 

前端代码

请求发送代码

const API_URL = "http://192.168.1.2:4000/api/admissionForm/submit";
const uploadFile = async (file: any) => {
  const token = JSON.parse(sessionStorage.getItem("user") || "{}").token;
  console.log(Object.keys(file))
  try {
    const config = {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "multipart/form-data",
        
      },
    };
    const res = await axios.post(API_URL, file, config);
    return res.data;
  } catch (err: any) {
    if (err.response.status === 500) {
      console.log("There was a problem with the server");
    } else {
      console.log(err.response.data.msg);
    }
  }
};

HTML表单

<form onSubmit={handleUpload}>
<input
              type="file"
              className=""
              id="inputFile01"
              style={{ visibility: "hidden" }}
              onChange={changeHandler}
              name="file"
            />
<button type="submit">
              Upload 
            </button>
</form>

文件选择与提交逻辑

const changeHandler = (e: SyntheticEvent) => {
    const target = e.target as HTMLInputElement;
    let selectedList = target.files as FileList;
    console.log(selectedList);
    let selected = selectedList[0] as File;

    setFile(selected);
  };

  const handleUpload = async (e: SyntheticEvent) => {
    e.preventDefault();
    console.log(file);
    if (file) {
      const formData = new FormData();
      formData.append("file", file);
      try {
        const res = await uploadFile(formData);
        console.log(res);
      } catch (error) {
        console.log(error);
      }
    }
  };

解决思路与修复方案

核心问题分析

  1. 前端Content-Type设置错误:手动指定"Content-Type": "multipart/form-data"会丢失axios自动生成的boundary分隔符,导致后端Busboy无法正确解析表单数据。
  2. Cloud Functions环境下请求体处理差异:本地Express默认不会解析multipart请求体,但Cloud Functions的运行环境可能已对req做了部分处理,直接使用busboy.end(req.rawBody)会导致数据不完整。
  3. 异步逻辑未正确等待:Busboy的finish事件是异步的,原代码未等待文件上传到Storage完成就结束请求,导致请求提前终止。
  4. 临时目录使用错误:原代码中使用./uploads/相对路径,Cloud Functions环境不存在该目录,应使用os.tmpdir()作为临时文件存储路径。

具体修复步骤

1. 前端代码修复

移除手动设置的Content-Type,让axios自动处理:

const uploadFile = async (file) => {
  const token = JSON.parse(sessionStorage.getItem("user") || "{}").token;
  try {
    const config = {
      headers: {
        Authorization: `Bearer ${token}`,
        // 移除手动设置的Content-Type
      },
    };
    const res = await axios.post(API_URL, file, config);
    return res.data;
  } catch (err) {
    if (err.response?.status === 500) {
      console.log("There was a problem with the server");
    } else {
      console.log(err.response?.data?.msg || err.message);
    }
  }
};

2. 后端代码修复

  • 使用req.pipe(busboy)替代busboy.end(req.rawBody),确保请求流完整传递给Busboy
  • 用Promise包裹Busboy逻辑,等待所有异步操作完成后再响应
  • 清理临时文件,避免占用内存
  • 处理Busboy的错误事件,防止未捕获异常

修复后的后端中间件:

const asyncHandler = require("express-async-handler");
const BusBoy = require("busboy");
const path = require("path");
const os = require("os");
const fs = require("fs");
const uuid = require("uuid");
const admin = require("firebase-admin");

const processFileMiddleware = asyncHandler(async (req, res) => {
  return new Promise((resolve, reject) => {
    const busboy = BusBoy({ headers: req.headers });

    let imageToBeUploaded = {};
    let imageFileName;
    const generatedToken = uuid.v4();

    busboy.on("file", (fieldname, file, { filename, encoding, mimetype }) => {
      console.log(fieldname, file, filename, encoding, mimetype);
      if (mimetype !== "image/jpeg" && mimetype !== "image/png") {
        return reject(new Error("Wrong file type submitted"));
      }
      // 简化文件扩展名获取
      const imageExtension = filename.split(".").pop();
      imageFileName = `${Math.round(Math.random() * 1000000000000)}.${imageExtension}`;
      const filepath = path.join(os.tmpdir(), imageFileName);
      imageToBeUploaded = { filepath, mimetype };
      file.pipe(fs.createWriteStream(filepath));
    });

    busboy.on("finish", async () => {
      try {
        // 等待文件上传到Storage完成
        await admin.storage().bucket().upload(imageToBeUploaded.filepath, {
          resumable: false,
          metadata: {
            metadata: {
              contentType: imageToBeUploaded.mimetype,
              firebaseStorageDownloadTokens: generatedToken,
            },
          },
        });
        // 清理临时文件
        fs.unlinkSync(imageToBeUploaded.filepath);
        res.status(200).json({ message: "File uploaded successfully" });
        resolve();
      } catch (err) {
        console.error("Storage upload error:", err);
        res.status(500).json({ error: "Failed to upload file" });
        reject(err);
      }
    });

    busboy.on("error", (err) => {
      console.error("Busboy parsing error:", err);
      res.status(400).json({ error: "Failed to parse form data" });
      reject(err);
    });

    // 正确传递请求流给Busboy
    req.pipe(busboy);
  });
});

module.exports = { processFileMiddleware };

3. 额外注意事项

  • 确保Express应用中没有在该中间件之前使用express.json()或express.urlencoded()这类解析请求体的中间件,否则会提前消耗请求流,导致Busboy无法获取数据。
  • Cloud Functions的临时目录是内存文件系统,上传文件大小不能超过函数的内存限制,大文件建议直接从前端上传到Cloud Storage,避免经过函数中转。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 15:20:33