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

express-validator误判合法输入,表单验证失败求助

使用express-validator 7.1.0验证multipart/form-data表单时请求体为空导致验证失败

问题背景

使用express-validator 7.1.0验证包含文件上传的EJS表单(编码为multipart/form-data),合法输入始终无法通过验证,日志显示req.body为空,但后续处理中间件能正常读取请求体。

相关代码

表单代码(EJS)

<%- include('partials/header') %>
    <h1>Feature-Request hinzufügen</h1>
    <body>
        <form class="addrequest" enctype="multipart/form-data" id="new_document_attachment" action="/auth/request/create" method="POST">
            <div class="form-group mb-2">
                <label for="title">Titel</label>
                <input type="text" class="form-control" id="title" name="title" maxlength="50" required>
            </div>
            <div class="form-group mb-2">
                <label for="description">Beschreibung</label>
                <textarea class="form-control" id="description" name="description" rows="10" maxlength="<%=MAX_DESCRIPTION_CHARACTER_INPUT%>" required></textarea>
            </div>
            <div class="mb-2">
                <label for="formFile" class="form-label">Dateianhang</label>
                <div class="file-loading">
                    <input id="input-folder-3" name="input-folder-3[]" type="file" accept=".jpeg,.jpg,.webp,.gif,.png.pdf" multiple>
                </div>
            </div>
            <div class="row">
                <div class="form-group mb-2 col">
                    <label class="mb-2">Kategorie <span id="howManyCategoriesMustBeProvided" class="text-danger-emphasis">(mindestens eine)</span></label>
                    <% requestOptions.categoriesFromDB.forEach(category => { %>
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" value="<%= category.category %>" id="category<%= category.id %>" name="category[]">
                            <label class="form-check-label text-<%= category.cssClass %>" for="category<%= category.id %>">
                                <%= category.category %>
                            </label>
                        </div>
                    <% }) %>
                </div>
                <% if (user && user.isAdmin) { %>
                    <div class="col">
                        <label class="mb-2">Status</label>
                        <% requestOptions.statusFromDB.forEach((status) => { %>
                            <div class="form-group mb-2">
                                <input class="form-check-input" type="radio" value="<%= status.id %>" 
                                    id="status<%= status.id %>" name="statusId" <%= status.id === ADMIN_DEFAULT_STATUS ? 'checked' : '' %>>
                                <label class="form-check-label text-<%= status.cssClass %>" for="status<%= status.id %>">
                                    <%= status.status %>
                                </label>
                            </div>
                        <% }) %>
                    </div>
                <% } %>
                <div class="col">
                    <label class="mb-2">Priorität</label>
                    <% requestOptions.prioritiesFromDB.forEach((priority, index) => { %>
                        <div class="form-group mb-2">
                            <input class="form-check-input" type="radio" value="<%= index +1 %>" 
                                id="<%= priority.priority %>" name="priority" <%= index === DEFAULT_PRIORITY ? 'checked' : '' %>>
                            <label class="form-check-label text-<%= priority.cssClass %>" for="<%= priority.priority %>">
                                <%= priority.priority %>
                            </label>
                        </div>
                    <% }) %>

                </div>
            </div>
            <span class="d-inline-block addrequest_submit_btn" 
            tabindex="0" 
            data-bs-toggle="popover" 
            data-bs-trigger="hover focus" 
            data-bs-placement="right" 
            data-bs-content="Du musst mindestens eine Kategorie auswählen.">
                <button class="btn btn-primary" type="submit" disabled>Zur Überprüfung einreichen</button>
            </span>
            <p>Du kannst deine Anfrage nach dem Einreichen nicht mehr Bearbeiten.</p>
              
        </form>
        <%- include('partials/footer') %>

express-validator工具封装(utils.js)

const { body, validationResult } = require("express-validator");

const ensureValid = (req, res, next) => {
  const result = validationResult(req);
  if (!result.isEmpty()) {
    return res.status(400).json({ errors: result.array() });
  }
  next();
};

module.exports = { body, ensureValid };

路由配置(app.js)

const authRoutes = require('./routes/authRoutes');
app.use('/auth', authRoutes);

验证链与路由处理(authRoutes.js)

const { body, ensureValid } = require('../utils');
const authController = require('../controllers/authController');

router.post('/request/create',

  body('title')
    .escape()
    .isString().withMessage('Der Titel muss ein String sein.')
    .isLength({ max: 40 }).withMessage('Der Titel darf maximal 40 Zeichen lang sein.'),
  body('description')
    .escape()
    .isString().withMessage('Die Beschreibung muss ein String sein.')
    .isLength({ min: 1, max: parseInt(process.env.MAX_DESCRIPTION_CHARACTER_INPUT, 10) || 1000 })
    .withMessage(`Die Beschreibung muss zwischen 1 und ${process.env.MAX_DESCRIPTION_CHARACTER_INPUT || 1000} Zeichen lang sein.`),
  body('category')
    .escape()
    .isString().withMessage('Ungültige Kategorie.')
    .custom((value, { req }) => {
      const validCategories = [
        process.env.categoryName1, process.env.categoryName2, process.env.categoryName3,
        process.env.categoryName4, process.env.categoryName5, process.env.categoryName6,
        process.env.categoryName7, process.env.categoryName8
      ];
      if (!validCategories.includes(value)) {
        throw new Error('Ungültige Kategorie.');
      }
      return true;
    }),
  body('priority')
    .escape()
    .isInt({ min: 1, max: 1 }).withMessage('Priorität muss eine gültige Nummer sein.'),
  body('statusId')
    .escape()
    .isInt({ min: 1, max: 4 }).withMessage('Status muss eine gültige Nummer (1-4) sein.'),
  ensureValid,

  utils.uploadStoring.array('input-folder-3[]', 5),
  authController.postNewRequest
);

测试输入

title: asdasd23
description: aer
input-folder-3[]: (binary)
category[]: Funktionswunsch
statusId: 2
priority: 2

验证错误信息

{
  "errors": [
    {
      "type": "field",
      "value": "",
      "msg": "Die Beschreibung muss zwischen 1 und 1000 Zeichen lang sein.",
      "path": "description",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Ungültige Kategorie.",
      "path": "category",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Priorität muss eine gültige Nummer sein.",
      "path": "priority",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Status muss eine gültige Nummer (1-4) sein.",
      "path": "statusId",
      "location": "body"
    }
  ]
}

排查现象

  • 全局日志显示POST /auth/request/create {},即req.body为空
  • 移除express-validator后,日志仍显示req.body为空,但authController.postNewRequest能正常读取请求体和文件

解决方案

1. 调整中间件顺序(核心问题)

multipart/form-data类型的请求需要通过multer解析后才能获取req.body,必须先执行multer解析中间件,再执行express-validator的验证链。

修改authRoutes.js中的路由顺序:

router.post('/request/create',
  // 先解析文件和表单数据
  utils.uploadStoring.array('input-folder-3[]', 5),
  // 再执行验证链
  body('title')
    .escape()
    .isString().withMessage('Der Titel muss ein String sein.')
    .isLength({ max: 40 }).withMessage('Der Titel darf maximal 40 Zeichen lang sein.'),
  // ... 其他验证规则
  ensureValid,
  // 最后执行控制器
  authController.postNewRequest
);

2. 修复数组字段category[]的验证

表单中category[]是数组类型,原代码无法正确读取数组,需调整为:

body('category')
  .isArray({ min: 1 }).withMessage('Mindestens eine Kategorie muss ausgewählt werden.'),
body('category.*')
  .escape()
  .isString().withMessage('Ungültige Kategorie.')
  .custom((value) => {
    const validCategories = [
      process.env.categoryName1, process.env.categoryName2, process.env.categoryName3,
      process.env.categoryName4, process.env.categoryName5, process.env.categoryName6,
      process.env.categoryName7, process.env.categoryName8
    ];
    if (!validCategories.includes(value)) {
      throw new Error('Ungültige Kategorie.');
    }
    return true;
  }),

3. 修复priority的验证规则

原代码限制priority只能为1,与测试输入冲突,需调整为表单实际的优先级范围(示例为1-3):

body('priority')
  .escape()
  .isInt({ min: 1, max: 3 }).withMessage('Priorität muss eine gültige Nummer sein.'),

4. 给statusId添加可选规则

statusId仅管理员可见,普通用户提交时无此字段,需添加optional():

body('statusId')
  .optional()
  .escape()
  .isInt({ min: 1, max: 4 }).withMessage('Status muss eine gültige Nummer (1-4) sein.'),

内容的提问来源于stack exchange,提问作者j-menter

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 04:54:52