Vercel部署Node.js后端上传图片时POST请求返回500错误求助
问题场景
部署在Vercel上的Node.js后端,更新用户信息时包含Cloudinary图片上传功能,发送带图片的POST请求会触发500内部服务器错误,服务器日志显示“Function Invocation Failed”;但不上传图片时API能正常运行。
错误日志
POST /quiz/userIndividualDetail 200 1919.543 ms - 602
Uncaught Exception: Error: ENOENT: no such file or directory, mkdir '/var/task/tmp'
at Object.mkdirSync (node:fs:1391:3)
at checkAndMakeDir (/var/task/node_modules/express-fileupload/lib/utilities.js:155:38)
at module.exports (/var/task/node_modules/express-fileupload/lib/tempFileHandler.js:14:3)
at Multipart. (/var/task/node_modules/express-fileupload/lib/processMultipart.js:75:9)
at Multipart.emit (node:events:517:28)
at HeaderParser.cb (/var/task/node_modules/busboy/lib/types/multipart.js:358:14)
at HeaderParser.push (/var/task/node_modules/busboy/lib/types/multipart.js:162:20)
at SBMH.ssCb [as _cb] (/var/task/node_modules/busboy/lib/types/multipart.js:394:37)
at feed (/var/task/node_modules/streamsearch/lib/sbmh.js:248:10)
at SBMH.push (/var/task/node_modules/streamsearch/lib/sbmh.js:104:16) {
errno: -2,
syscall: 'mkdir',
code: 'ENOENT',
path: '/var/task/tmp'
}
原代码
handleUpdateUser函数
const handleUpdateUser = async (req, res) => { const { name, email, password, gender, role } = req.body; let userimage = ""; try { // Check if all required fields are provided if (!name || !email || !password || !gender || !role) { return res.status(400).send({ status: false, message: "All fields are required" }); } // Check if the email exists in the database let result = await AuthModal.findOne({ email }); if (!result) { return res.status(400).send({ status: false, message: "Email does not exist in the database" }); } // Prepare the object with new user data let newObj = { name, email, password, gender, role }; // Handle image file attachment if (req.files) { const files = req.files.image; // Ensure the image file is provided if (!files) { return res.status(400).send({ status: false, message: "Image Key is required" }); } // Check if tempFilePath exists before uploading if (files.tempFilePath !== null) { const uploadResult = await cloudinary.uploader.upload(files.tempFilePath); if (uploadResult) { // Remove the temp file after successful upload fs.unlinkSync(files.tempFilePath); if (userimage === "") { newObj.userimage = uploadResult.secure_url; } } } } // Update user data in the database let updateDB_Result = await AuthModal.updateOne( { email }, { $set: newObj }, { returnDocument: 'after' } ); const { matchedCount } = updateDB_Result; if (matchedCount === 1) { return res.status(200).send({ status: true, message: "Your changes have been successfully updated", data: newObj }); } } catch (error) { console.error(error); return res.status(500).send({ status: false, message: "An internal server error occurred" }); } };
express-fileupload中间件配置
const fileUpload = require('express-fileupload'); const path = require('path'); app.use(fileUpload({ useTempFiles: true, tempFileDir: path.join(__dirname, '../tmp/') // Temp directory for uploaded files }));
错误分析
核心问题是Vercel Serverless函数的文件系统限制:
- Vercel的Serverless环境中,只有
/tmp目录是可写的,且该目录由平台自动创建,无需手动生成 - 原配置的
tempFileDir: path.join(__dirname, '../tmp/')在本地开发时能正常指向项目的tmp目录,但部署到Vercel后,项目代码会被打包到/var/task目录,此时路径变成/var/task/tmp,这个目录不存在且无权限创建,导致express-fileupload创建临时目录时抛出ENOENT错误。
另外代码还有几个潜在问题:
files.tempFilePath在useTempFiles: true配置下必然存在,无需判断是否为nullfs.unlinkSync是同步操作,若临时文件已被删除会抛出错误,需添加异常捕获updateOne的returnDocument: 'after'选项无效,MongoDB的updateOne只返回修改计数,若要获取更新后的文档,应该用findOneAndUpdate
解决方案
1. 修正临时文件目录配置
将express-fileupload的tempFileDir直接设置为Vercel允许的/tmp目录:
const fileUpload = require('express-fileupload'); app.use(fileUpload({ useTempFiles: true, tempFileDir: '/tmp/' // 直接使用Vercel提供的可写临时目录 }));
2. 优化handleUpdateUser函数
修复潜在问题,确保代码在Serverless环境稳定运行:
const handleUpdateUser = async (req, res) => { const { name, email, password, gender, role } = req.body; try { // 校验必填字段 if (!name || !email || !password || !gender || !role) { return res.status(400).send({ status: false, message: "所有字段均为必填项" }); } // 检查用户是否存在 const existingUser = await AuthModal.findOne({ email }); if (!existingUser) { return res.status(400).send({ status: false, message: "该邮箱未在数据库中注册" }); } // 准备更新数据 const updateData = { name, email, password, gender, role }; // 处理图片上传 if (req.files?.image) { const imageFile = req.files.image; try { // 上传到Cloudinary const uploadResult = await cloudinary.uploader.upload(imageFile.tempFilePath); if (uploadResult) { updateData.userimage = uploadResult.secure_url; // 尝试删除临时文件,忽略删除失败的情况 try { fs.unlinkSync(imageFile.tempFilePath); } catch (unlinkError) { console.warn("临时文件删除失败:", unlinkError); } } } catch (uploadError) { console.error("图片上传失败:", uploadError); return res.status(500).send({ status: false, message: "图片上传失败" }); } } // 更新用户信息并返回更新后的文档 const updatedUser = await AuthModal.findOneAndUpdate( { email }, { $set: updateData }, { new: true, runValidators: true } ); if (updatedUser) { return res.status(200).send({ status: true, message: "用户信息更新成功", data: updatedUser }); } else { return res.status(404).send({ status: false, message: "用户不存在" }); } } catch (error) { console.error("服务器内部错误:", error); return res.status(500).send({ status: false, message: "服务器内部错误" }); } };
关键修改点
- 直接使用
/tmp作为临时文件目录,适配Vercel环境 - 简化图片上传逻辑,移除不必要的
tempFilePath判断 - 为
fs.unlinkSync添加try-catch,避免删除失败导致整个请求崩溃 - 替换
updateOne为findOneAndUpdate,并使用new: true获取更新后的用户文档,同时开启runValidators确保数据校验生效 - 细化错误提示,方便排查问题
内容的提问来源于stack exchange,提问作者Asim Atiq Shah

