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

Node.js中使用Express Validator与Multer时的表单验证异常及文件上传疑问

Node.js中使用Express Validator与Multer时的表单验证异常及文件上传疑问

问题原因分析

你遇到的问题核心在于中间件执行顺序和请求体解析逻辑:

  1. 你的server.js只配置了bodyParser.json(),它只能处理JSON格式的请求体,完全无法解析form-data类型的表单数据。
  2. 路由里你把Express Validator的验证中间件放在了Multer的upload前面——此时req.body还没被解析(form-data需要Multer来解析),所以验证时title和content字段是空的,自然触发验证失败。
  3. 验证失败后Multer仍然会执行:因为Express中间件是按顺序执行的,验证中间件先返回错误,但upload中间件已经完成了文件上传操作,所以Cloudinary里会出现成功上传的图片。

解决方案

调整路由中间件的顺序,让Multer的upload先执行,这样它会先解析form-data,把title、content等字段存入req.body,之后验证中间件就能读取到正确的字段值:

修改postRoutes.js中的POST路由:

router.post(
  "/posts/new",
  upload, // 先让multer解析form-data
  [
    body("title")
      .trim()
      .isLength({ min: 5 })
      .notEmpty()
      .withMessage("Title is required"),
    body("content")
      .trim()
      .isLength({ min: 5 })
      .notEmpty()
      .withMessage("Content is required"),
  ],
  postController.createPost
);

额外说明

  • 不需要额外添加bodyParser.urlencoded(),Multer已经完全负责处理form-data格式的请求体解析。
  • 如果想避免验证失败时文件被上传,你需要在控制器里手动删除Cloudinary上的文件,或者在Multer的回调里添加前置判断逻辑——不过通常这种场景下,先上传文件再验证是可以接受的,毕竟文件上传的开销不大,后续删除也很方便。

原问题详情

以下是你的问题及代码参考:

问题背景

我正在用Express开发Node.js后端,创建带文件上传的新帖子时遇到了验证错误。明明在Postman的form-data里填了必填的标题和内容,却收到这两个字段为空的验证提示,但图片却成功传到了Cloudinary。

代码实现

Middleware (uploadImage.js):

const cloudinary = require("cloudinary").v2;
const { CloudinaryStorage } = require("multer-storage-cloudinary");
const multer = require("multer");

// Configure Cloudinary storage for multer
const storage = new CloudinaryStorage({
  cloudinary: cloudinary,
  params: {
    folder: "post/uploads",
    public_id: (req, file) => file.originalname,
    resource_type: "image", // Ensure the resource type is image
    transformation: [
      { width: 1000, crop: "scale" }, // Resize the image to a width of 1000 pixels
      { quality: "auto:best" }, // Automatically adjust the best quality
      { fetch_format: "auto" }, // Automatically convert to the best format
    ],
  },
});

// Multer upload middleware
const upload = multer({ storage }).fields([
  { name: "imageUri", maxCount: 1 },
  { name: "thumbnails", maxCount: 4 },
]);

module.exports = upload;

Routes (postRoutes.js):

const express = require("express");
const router = express.Router();
const { body } = require("express-validator");
const postController = require("../controller/postController");
const upload = require("../middleware/uploadImage");

router.get("/posts", postController.getPosts);
router.post(
  "/posts/new",
  [
    body("title")
      .trim()
      .isLength({ min: 5 })
      .notEmpty()
      .withMessage("Title is required"),
    body("content")
      .trim()
      .isLength({ min: 5 })
      .notEmpty()
      .withMessage("Content is required"),
  ],
  upload,
  postController.createPost
);

router.get("/posts/:id", postController.getPostById);

router.put(
  "/posts/:id",
  [
    body("title")
      .trim()
      .isLength({ min: 5 })
      .notEmpty()
      .withMessage("Title is required"),

    body("content")
      .trim()
      .isLength({ min: 5 })
      .notEmpty()
      .withMessage("Content is required"),
  ],
  postController.updatePostById
);

router.delete("/posts/:id", postController.deletePostById);

module.exports = router;

Controller (postController.js):

exports.createPost = async (req, res, next) => {
  console.log("Request Body:", req.body);
  console.log("Request Files:", req.files);
  const { title, content } = req.body;
  const error = validationResult(req);

  if (!error.isEmpty()) {
    return res.status(400).json({
      status: "Failed",
      code: 400,
      message: "Validation failed",
      errors: error.array(),
    });
  }

  try {
    const imageUri = req.files.imageUri ? req.files.imageUri[0].path : null;
    const thumbnails = req.files.thumbnails ? req.files.thumbnails.map(file => file.path) : [];

    const post = new Post({
      title,
      content,
      imageUri,
      thumbnails,
    });

    const result = await post.save();

    return res.status(201).json({
      status: "Success",
      code: 201,
      message: "Post created successfully",
      data: result,
    });
  } catch (error) {
    return res.status(500).json({
      status: "Failed",
      code: 500,
      message: "An error occured while creating post",
      error,
    });
  }
};

Model (postModel.js):

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const postSchema = new Schema(
  {
    title: {
      type: String,
      required: true,
    },
    content: {
      type: String,
      required: true,
    },
    imageUri: {
      type: String,
      required: false,
    },
    thumbnails: {
      type: [String],
      required: false,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("post", postSchema);

Server Setup (server.js):

require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const postRoutes = require("./routes/postRoutes");
const cloudinary = require('./config/CloudinaryConfig')

const hostname = "localhost";
const port = 8080;

const MONGODB_URI = process.env.MONGODB_URI

const app = express();

app.use(bodyParser.json());

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET , POST , PUT , DELETE");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type , Authorization");
  next();
});

app.use("/", postRoutes);

mongoose
  .connect(MONGODB_URI)
  .then(() => {
    console.log("Connected to database");
    app.listen(port, hostname, () => {
      console.log(`Server is running at http://${hostname}:${port}`);
    });
  })
  .catch((err) => {
    console.log(err);
  });

Postman请求信息

  • Method: POST
  • URL: http://localhost:3000/api/posts/new
  • Body: form-data
    • title: First Post
    • content: Hello World
    • imageUri:(file)
    • thumbnails: (file)

服务器日志

[nodemon] starting `node app.js`
Connected to database
Server is running at http://localhost:8080
Request Body: [Object: null prototype] {
  title: 'First Post',
  content: 'Hello World'
}
Request Files: [Object: null prototype] {
  imageUri: [
    {
      fieldname: 'imageUri',
      originalname: 'Leonardo_Kino_XL_wildlife_jungle_and_include_all_animals_2.jpg',
      encoding: '7bit',
      mimetype: 'image/jpeg',
      path: 'https://res.cloudinary.com/du9ze60lr/image/upload/v1738605266/post/uploads/Leonardo_Kino_XL_wildlife_jungle_and_include_all_animals_2.jpg.jpg',
      size: 190411,
      filename: 'post/uploads/Leonardo_Kino_XL_wildlife_jungle_and_include_all_animals_2.jpg'
    }
  ],
  thumbnails: [
    {
      fieldname: 'thumbnails',
      originalname: 'NEW_Vision.webp',
      encoding: '7bit',
      mimetype: 'image/webp',
      path: 'https://res.cloudinary.com/du9ze60lr/image/upload/v1738605268/post/uploads/NEW_Vision.webp.png',
      size: 48171,
      filename: 'post/uploads/NEW_Vision.webp'
    }
  ]
}

错误响应

{
  "status": "Failed",
  "code": 400,
  "message": "Validation failed",
  "errors": [
    { "type": "field", "value": "", "msg": "Invalid value", "path": "title", "location": "body" },
    { "type": "field", "value": "", "msg": "Title is required", "path": "title", "location": "body" },
    { "type": "field", "value": "", "msg": "Invalid value", "path": "content", "location": "body" },
    { "type": "field", "value": "", "msg": "Content is required", "path": "content", "location": "body" }
  ]
}

备注:内容来源于stack exchange,提问作者Prince

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 08:53:10