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

React Hook Form表单提交为何以application/json发送并返回400错误?

问题:React Hook Form表单提交FormData时Content-Type异常导致后端验证失败

我用react-hook-form开发酒店信息添加表单,包含图片上传字段。提交时把表单数据转成FormData发送到API,但请求的Content-Type被自动设为application/json; charset=utf-8,后端返回所有字段必填的400错误。手动设置Content-Type为multipart/formdata又出现「Multipart: Boundary not found」错误。


前端代码

表单处理逻辑

const formMethods = useForm<HotelFormData>();
const { handleSubmit } = formMethods;

const onSubmit = handleSubmit((data: HotelFormData) => {
  const formData = new FormData();
  formData.append("name", data.name);
  formData.append("city", data.city);
  formData.append("country", data.country);
  formData.append("description", data.description);
  formData.append("type", data.type);
  formData.append("pricePerNight", data.pricePerNight.toString());
  formData.append("starRating", data.starRating.toString());
  formData.append("adultCapacity", data.adultCapacity.toString());
  formData.append("childCapacity", data.childCapacity.toString());
  data.facilities.forEach((facility, index) => {
    formData.append(`facilities[${index}]`, facility);
  });
  Array.from(data.imageFiles).forEach((imageFile) => {
    formData.append("imageFiles", imageFile);
  });
  onSave(formData);
});

API请求函数

export const addMyHotel = async (formData: FormData) => {
    const response = await fetch(`${BASE_URL}/api/my-hotels`, {
        method: "POST",
        credentials: "include",
        body: formData,
    });
    const resBody = await response.json();
    if (!response.ok) {
        throw new Error(resBody.message);
    }
    return resBody;
};

后端API代码(/api/my-hotels)

import express from "express";
import { Request, Response } from "express";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import mime from "mime-types";
import { client } from "../index";
import multer from "multer";
import Hotel, { HotelType } from "../models/hotel";
import verifyToken from "../middleware/auth";
import { body, validationResult } from "express-validator";

const router = express.Router();

const storage = multer.memoryStorage();
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5 * 1024 * 1024,
  },
});

router.post(
  "/",
  verifyToken,
  [
    body("name").notEmpty().withMessage("Name is required"),
    body("city").notEmpty().withMessage("City is required"),
    body("country").notEmpty().withMessage("Country is required"),
    body("description").notEmpty().withMessage("Description is required"),
    body("type").notEmpty().withMessage("Type is required"),
    body("pricePerNight")
      .notEmpty()
      .isNumeric()
      .withMessage("Price per night must be a number"),
    body("adultCapacity")
      .notEmpty()
      .isNumeric()
      .withMessage("Adult capacity must be a number"),
    body("childCapacity")
      .notEmpty()
      .isNumeric()
      .withMessage("Child capacity must be a number"),
    body("starRating")
      .notEmpty()
      .isNumeric()
      .withMessage("Star rating must be a number"),
    body("facilities")
      .notEmpty()
      .isArray()
      .withMessage("Facilities must be an array"),
  ],
  upload.array("imageFiles", 6),
  async (req: Request, res: Response) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res
        .status(400)
        .json({ message: "Bad Request", errors: errors.array() });
    }
    try {
      const files = req.files as Express.Multer.File[];
      const promises = files.map(async (file) => {
        const fileExt = mime.extension(file.mimetype);
        if (!fileExt) {
          throw new Error("Invalid file type");
        }
        const params = {
          Bucket: process.env.S3_BUCKET as string,
          Key: `${Date.now()}.${fileExt}`,
          Body: file.buffer,
          ContentType: file.mimetype,
        };
        await client.send(new PutObjectCommand(params));
        const link = `https://${process.env.S3_BUCKET_NAME}.${process.env.S3_REGION}.amazonaws.com/${Date.now()}`;
        return link;
      });
      const imageUrls = await Promise.all(promises);
      const newHotel: HotelType = req.body;
      newHotel.imageUrls = imageUrls;
      newHotel.lastUpdated = new Date();
      newHotel.userId = req.userId;
      const hotel = new Hotel(newHotel);
      await hotel.save();
      res.status(201).send(hotel);
    } catch (error) {
      console.log("error creating hotel", error);
      res.status(500).json({ message: "Internal server error" });
    }
  }
);

export default router;

返回的错误信息

Bad Request - errors:
0: {type: "field", msg: "Name is required", path: "name", location: "body"}
1: {type: "field", msg: "City is required", path: "city", location: "body"}
2: {type: "field", msg: "Country is required", path: "country", location: "body"}
3: {type: "field", msg: "Description is required", path: "description", location: "body"}
4: {type: "field", msg: "Type is required", path: "type", location: "body"}
5: {type: "field", msg: "Invalid value", path: "pricePerNight", location: "body"}
6: {type: "field", msg: "Price per night must be a number", path: "pricePerNight", location: "body"}
7: {type: "field", msg: "Invalid value", path: "adultCapacity", location: "body"}
8: {type: "field", msg: "Adult capacity must be a number", path: "adultCapacity", location: "body"}
9: {type: "field", msg: "Invalid value", path: "childCapacity", location: "body"}
10: {type: "field", msg: "Child capacity must be a number", path: "childCapacity", location: "body"}
11: {type: "field", msg: "Invalid value", path: "starRating", location: "body"}
12: {type: "field", msg: "Star rating must be a number", path: "starRating", location: "body"}
13: {type: "field", msg: "Invalid value", path: "facilities", location: "body"}
14: {type: "field", msg: "Facilities must be an array", path: "facilities", location: "body"}


请求Payload

name: My Hotel
city: Iwdj
country: Pdihuf
description: poiuytrewqasdfghjkllllllllllllllllkmnbvcxz
type: hotel
pricePerNight: 7
starRating: 3
adultCapacity: 8
childCapacity: 7
facilities[0]: free parking
facilities[1]: swimming pool
imageFiles: (binary)
imageFiles: (binary)

解决方案

核心问题分析

  1. Content-Type自动设为application/json:大概率是表单组件未正确设置enctype,或请求拦截器强制修改了请求头。
  2. 手动设置multipart/formdata导致Boundary缺失:浏览器会自动为FormData生成带boundary的Content-Type(格式为multipart/form-data; boundary=----WebKitFormBoundaryXXXX),手动设置会覆盖该头,导致后端无法解析表单数据。
  3. 后端中间件顺序错误:multer需要先解析FormData,才能让express-validator拿到req.body中的字段,当前顺序导致验证时字段未被填充。

具体修复步骤

1. 正确设置表单enctype

如果使用原生form标签,必须指定enctype="multipart/form-data":

<form onSubmit={handleSubmit(onSubmit)} enctype="multipart/form-data">
  {/* 表单字段 */}
</form>

2. 禁止手动设置Content-Type

在API请求函数中,不要手动添加Content-Type头,让浏览器自动处理:

export const addMyHotel = async (formData: FormData) => {
    const response = await fetch(`${BASE_URL}/api/my-hotels`, {
        method: "POST",
        credentials: "include",
        body: formData,
        // 移除手动设置的Content-Type头
    });
    // ... 其他代码
};

3. 调整后端中间件顺序

将multer解析中间件移到验证规则前面,确保先解析FormData再验证字段:

router.post(
  "/",
  verifyToken,
  upload.array("imageFiles", 6), // 先解析FormData
  [
    // 所有验证规则
    body("name").notEmpty().withMessage("Name is required"),
    // ... 其他验证规则
  ],
  async (req: Request, res: Response) => {
    // ... 处理逻辑
  }
);

4. 修复S3链接生成bug

当前代码中Date.now()执行两次,导致S3存储的文件名和返回的链接不一致,修改为:

const timestamp = Date.now();
const params = {
  Bucket: process.env.S3_BUCKET as string,
  Key: `${timestamp}.${fileExt}`,
  Body: file.buffer,
  ContentType: file.mimetype,
};
await client.send(new PutObjectCommand(params));
const link = `https://${process.env.S3_BUCKET_NAME}.${process.env.S3_REGION}.amazonaws.com/${timestamp}.${fileExt}`;

验证修复效果

提交表单后,请求头的Content-Type应显示为带boundary的multipart/form-data格式,后端可正确解析req.body和req.files,验证通过后正常创建酒店并返回结果。


内容的提问来源于stack exchange,提问作者Noor Fatima

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 14:30:05