React+Node.js文件上传在Cpanel生产环境异常求助
问题描述
React.js客户端向Node.js服务端上传文件时出现异常:选择图片点击提交后,加载状态持续但无结果(无数据库响应),具体特征:
- 仅在Cpanel部署的生产环境出现该问题,开发环境文件上传功能正常。
- 编辑文本、修改状态等其他请求在开发与生产环境均正常,仅文件上传请求异常。
- 生产环境中偶尔能成功上传文件,但多数时候会持续加载无响应。
相关代码
client.js
const updateCoverImage = async (formData)=> { try { const response = await fetch(api+"/update-cover-image/cover", { method: 'PATCH', credentials: 'include', mode: 'cors', body: formData }).then(response=> response.json()); if(!response?.data) { throw new Error(response.message); } return response.data; } catch (error) { return error.message; } }
controller.js
const updateCoverImage = async (req, res) => { try { const { id } = req.user; let { filename } = req.file; const { folder } = req.params; if (!filename) { return view.error(res, 400, "Please provide cover image"); } filename = api_url + "assets/restaurant/" + folder + "/" + filename; await connectDb(async pool => { // get old cover image const [oldCoverImage] = await pool.query("SELECT cover_image FROM restaurant WHERE user_id =?", [id]); if (!oldCoverImage) { return view.error(res, 404, "Restaurant not found"); } const cover_image = oldCoverImage[0].cover_image.split("/cover/")[1]; // remove old cover image from directory const isFileExist = fs.existsSync(path.join(__dirname, astP + "restaurant/cover/" + cover_image)); if (isFileExist) { deleteFile(path.join(__dirname, astP + "restaurant/cover/" + cover_image)); } const [rows] = await pool.query("UPDATE restaurant SET cover_image =? WHERE user_id =?", [filename, id]); if (rows.affectedRows === 0) { return view.error(res, 404, "Restaurant not found"); } return view.success(res, { message: "Cover image updated successfully" }); }) } catch (error) { // console.log(error); return view.error(res, 500, "An error occurred. Please try again later."); } }
router.js
router.patch("/update-cover-image/:folder", protectRouteToken, upload(astP+"restaurant/").single("image"), updateCoverImage);
multer.js
const multer = require('multer'); const path = require("path"); const fs = require('fs'); const upload = (newPath) => { try { const storage = multer.diskStorage({ destination: (req, file, cb) => { const destination = path.join(__dirname, newPath, req.params.folder || ""); if (!fs.existsSync(destination)) { fs.mkdirSync(destination); } cb(null, destination); }, filename: (req, file, cb) => { const src = Date.now() + '-' + "welqr." + file.originalname.split(".")[file.originalname.split(".").length - 1]; cb(null, src); } }); const uploadFile = multer({ storage: storage, limits: { fieldSize: 1024 * 1024 * 5 } // 5MB }) return uploadFile; } catch (error) { console.log(error); return null; } } module.exports = { upload };
排查与解决方案
1. 修复Cpanel文件权限问题
Cpanel环境下Node进程的运行用户(通常为虚拟主机用户或nobody)可能缺少目标目录的读写权限:
- 检查
astP+"restaurant/cover/"对应实际目录的权限,设置为目录755、文件644; - 替换Multer中同步创建目录的代码为异步递归创建,同时指定权限:
destination: (req, file, cb) => { const destination = path.join(__dirname, newPath, req.params.folder || ""); fs.mkdir(destination, { recursive: true, mode: 0o755 }, (err) => { if (err) return cb(err); cb(null, destination); }); }
2. 替换同步IO操作,避免阻塞事件循环
生产环境资源受限,同步IO会阻塞Node事件循环导致请求超时:
- 把
fs.existsSync替换为异步的fs.access:// 替换原删除旧文件的同步判断逻辑 fs.access(path.join(__dirname, astP + "restaurant/cover/" + cover_image), fs.constants.F_OK, (err) => { if (!err) { fs.unlink(path.join(__dirname, astP + "restaurant/cover/" + cover_image), (unlinkErr) => { if (unlinkErr) console.error("删除旧文件失败:", unlinkErr); }); } }); - 确保
deleteFile使用异步的fs.unlink而非同步版本。
3. 完善Multer错误处理
当前代码未处理Multer上传错误(如文件超限、类型不匹配),会导致请求挂起:
router.patch("/update-cover-image/:folder", protectRouteToken, (req, res, next) => { upload(astP+"restaurant/").single("image")(req, res, (err) => { if (err instanceof multer.MulterError) { return view.error(res, 400, `上传错误: ${err.message}`); } else if (err) { return view.error(res, 500, `服务器错误: ${err.message}`); } next(); }); }, updateCoverImage);
4. 确认数据库连接池的异步逻辑
检查connectDb是否正确返回Promise,否则await connectDb(...)会导致逻辑卡住:
const connectDb = (callback) => { return new Promise((resolve, reject) => { pool.getConnection((err, conn) => { if (err) return reject(err); try { const result = callback(conn); resolve(result); } catch (e) { reject(e); } finally { conn.release(); } }); }); };
5. 添加请求超时处理
前端fetch未设置超时,浏览器会无限等待;同时检查Cpanel的Apache/Nginx超时配置(如Timeout、LimitRequestBody):
const updateCoverImage = async (formData)=> { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时 const response = await fetch(api+"/update-cover-image/cover", { method: 'PATCH', credentials: 'include', mode: 'cors', body: formData, signal: controller.signal }).then(response=> response.json()); clearTimeout(timeoutId); if(!response?.data) { throw new Error(response.message); } return response.data; } catch (error) { return error.message; } }
内容的提问来源于stack exchange,提问作者Adil Akothiat
相关产品推荐
相关产品推荐

