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

React前端提交带图片表单至后端报错400:images字段不被允许

React前端提交图片到Multer后端报错的解决方案

问题描述

在React前端将图片加入postValues后发送至后端接口,后端的multer、路由、控制器及发布服务通过Insomnia测试正常,但前端表单提交图片时出现400错误,提示"images" is not allowed。打印postValues可见图片数据格式正确,但提交方式存在问题。

相关代码

前端代码

/* <----------------------- MODULOS --------------------------> */
import Creatable, { useCreatable } from 'react-select/creatable';

/* <----------------------- FUNCIONES --------------------------> */
import { useState, useEffect } from "react";

/* <---------------- COMPONENTES MATERIAL UI -------------------> */
import Avatar from '@mui/material/Avatar';

/* <----------------------- ICONOS --------------------------> */
import Button from '@mui/material/Button';
import SendIcon from '@mui/icons-material/Send';

/* <----------------------- SERVICIOS  -----------------------< */
import { getHashtags } from "@/services/hashtag.service";
import { createPost } from "@/services/post.service";

/* <----------------------- CONTEXTO  -----------------------< */
import { useAuth } from '@/context/AuthContext.jsx';

const CreatePost = () => {

  const { user } = useAuth();

  const [postValues, setPostValues] = useState({
    author: user.id,
    title: "",
    description: "",
    images: [],
    hashtags: []
  });

  const [hashtagValues, setHashtagValues] = useState("");

  const getDataHashtags = async () =>{
    try {
      const response = await getHashtags();
      const optionsHashtags = response.data.data.data.map(hashtag => ({
        // No se utiliza el id, por como se programo en el backend -> value: hashtag._id,
        value: hashtag.nameHashtag,
        label: hashtag.nameHashtag
      }))
      setHashtagValues(optionsHashtags);
    } catch (error) {
      console.log(error);
    }
  }

  useEffect( () => {
    getDataHashtags();
  },[])

  // Manejo de campos simples
  const handleChange = (e) => {
    const { name, value } = e.target;
    setPostValues( (prevState) => ({
        ...prevState,
        [name]: value      
      })
    )
  }

  // Manejo de hashtags
  const handleHashtagChange  = (selectedOptions) => {
    const hashtags = selectedOptions ? selectedOptions.map(option => option.value) : [];
    setPostValues((prevState) => ({
      ...prevState,
      hashtags
    }));
  };

  const handleImageChange = (e) => {
    const files = Array.from(e.target.files);

    setPostValues((prevState) => (
      {...prevState, images: [...prevState.images, ...files]}
  ));
  }
  


  // Envio de postValues
  const onSubmit = async (e) => {
    e.preventDefault();
    try {
      await createPost(postValues);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <div className='flex flex-row p-4 w-3/4 mx-auto space-x-2'>

      <div>
        <Avatar/>
      </div>
        
      <form className="flex flex-col w-full" encType="multipart/form-data">
        <input placeholder='Titulo' name="title" value={postValues.title} onChange={handleChange}
            className='bg-gray-100 p-2 mb-4 border-2 rounded-md leading-5 transition duration-150 ease-in-out sm:text-sm sm:leading-5 focus:outline-none focus:border-blue-500 flex w-full'>
        </input>

        <textarea id="postContent" rows="4" placeholder="Cuentanos más sobre lo que quieres publicar :-)" onChange={handleChange} name="description" value={postValues.description}
            className="bg-gray-100 p-2 mb-2  rounded-md px-4 py-2 leading-5 transition duration-150 ease-in-out sm:text-sm sm:leading-5 resize-none focus:outline-none focus:border-blue-500" >
        </textarea>



        <Creatable isMulti placeholder="Hashtags" onChange={handleHashtagChange}
          className="bg-gray-100 mb-2 rounded-md leading-5 transition duration-150 ease-in-out sm:text-sm sm:leading-5 focus:outline-none focus:border-blue-500"
          options= {hashtagValues}
          >
        </Creatable>

        <div className="mb-2">
          <div className="mb-2">
            <input type="file" multiple onChange={handleImageChange} />
          </div>
        </div>

        <div className="grid grid-cols-2 gap-2 mb-4">
          {postValues.images.map((image, index) => (
            <div key={index} className="relative">
              <img src={URL.createObjectURL(image)} alt={`upload-${index}`} className="w-full h-full object-cover rounded-md" />
            </div>
          ))}
        </div>

        <Button variant="contained" endIcon={<SendIcon />}
          className="rounded"
          onClick={onSubmit}
        >
          Publicar
        </Button>
      </form>
    </div>
  )
}

export default CreatePost

后端路由

router.post("/createPost",[uploadImage.array('images')], postController.createPost);

Multer配置

/* <---------------------- MODULES ---------------------- ----> */
const multer = require('multer');

// storage: Memory storage configuration
const storage = multer.memoryStorage();
// limits: File size limit (5MB)
const limits = { fileSize: 1024 * 1024 * 5 };

// filter: Filter function to allow only image files (PNG, JPEG, JPG)
const filter = (req, file, cb) => {
     if(file && (file.mimetype === 'image/png' || file.mimetype === 'image/jpeg' || file.mimetype === 'image/jpg')){
         cb(null, true);
     }else{
         cb(null, false);
     }
}

// Multer configuration
const uploadImage = multer({
     storage: storage,
     fileFilter: filter,
     limits
});

module.exports = uploadImage;

报错信息

AxiosError {
  message: 'Request failed with status code 400',
  name: 'AxiosError',
  code: 'ERR_BAD_REQUEST',
  config: {...},
  request: XMLHttpRequest,
  response: {
    config: {...},
    data: {
      state: 'Error',
      message: '"images" is not allowed',
      details: {...}
    },
    headers: AxiosHeaders {
      content-length: '68',
      content-type: 'application/json; charset=utf-8'
    },
    request: XMLHttpRequest,
    status: 400,
    statusText: 'Bad Request',
    [[Prototype]]: Object
  },
  stack: 'AxiosError: Request failed with status code 400\n    at settle (webpack-internal:///./node_modules/axios/lib/core/settle.js:24:12)\n    at XMLHttpRequest.onloadend (webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:125:66)\n    at Axios.request (webpack-internal:///./node_modules/axios/lib/core/Axios.js:54:41)\n    at async onSubmit (webpack-internal:///./src/components/CreatePost.jsx:80:13)',
  [[Prototype]]: Error
}

问题原因

直接通过axios发送包含File对象的JSON数据时,File会被序列化为空对象{},后端Multer无法识别到文件,同时后端的请求验证(如Joi/Express Validator)会认为images字段格式不符合要求,因此返回"images" is not allowed的错误。

解决方案

必须使用FormData格式发送文件和表单数据,这是Multer能正确解析文件的唯一格式。

1. 修改前端提交逻辑

更新onSubmit函数,将postValues转换为FormData:

const onSubmit = async (e) => {
  e.preventDefault();
  try {
    const formData = new FormData();
    // 追加普通字段
    formData.append('author', postValues.author);
    formData.append('title', postValues.title);
    formData.append('description', postValues.description);
    // 追加数组类型的hashtags(后端需支持数组解析)
    postValues.hashtags.forEach(hashtag => {
      formData.append('hashtags[]', hashtag);
    });
    // 追加图片,对应Multer的array('images')配置
    postValues.images.forEach(image => {
      formData.append('images', image);
    });
    // 发送FormData
    await createPost(formData);
  } catch (error) {
    console.log(error);
  }
};

2. 调整Axios请求配置

确保createPost服务中不手动设置Content-Type,Axios会自动为FormData添加正确的multipart/form-data头及边界标识:

// post.service.js
import axios from 'axios';

export const createPost = async (formData) => {
  return await axios.post('/api/posts/createPost', formData, {
    headers: {
      // 不要手动设置Content-Type,让Axios自动处理
      // 'Content-Type': 'multipart/form-data' ❌ 移除这行
    }
  });
};

3. 可选:后端数组字段解析

如果后端无法直接解析hashtags[]数组,可在后端使用express.urlencoded({ extended: true })中间件,确保能正确接收数组类型的表单数据。

内容的提问来源于stack exchange,提问作者Matias San Martin M.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 07:45:57