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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 07:20:05