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); } } };
解决思路与修复方案
核心问题分析
- 前端Content-Type设置错误:手动指定
"Content-Type": "multipart/form-data"会丢失axios自动生成的boundary分隔符,导致后端Busboy无法正确解析表单数据。 - Cloud Functions环境下请求体处理差异:本地Express默认不会解析multipart请求体,但Cloud Functions的运行环境可能已对req做了部分处理,直接使用
busboy.end(req.rawBody)会导致数据不完整。 - 异步逻辑未正确等待:Busboy的
finish事件是异步的,原代码未等待文件上传到Storage完成就结束请求,导致请求提前终止。 - 临时目录使用错误:原代码中使用
./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
相关产品推荐
相关产品推荐

