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

React/Redux提交FormData的PUT请求中Express的req.params.id未定义问题

问题描述

使用FormData通过PUT请求提交多部分表单时,触发错误:Cast to objectId failed for value 'undefined' (type string) at path '_id' for model 'Blogpost',后端控制器中console.log(req.params.id)返回undefined。

技术栈:react-hook-form、redux-toolkit、multer-cloudinary实现图片上传。

核心现象:

  • 仅编辑文本(不上传图片)时功能正常
  • 上传图片时触发错误
  • 后端能获取req.body,图片也能成功上传,但req.params.id为undefined
  • POST请求使用相同代码可正常运行

前端组件代码(BlogpostEdit)

const BlogpostEdit = () => {
  const { id } = useParams()

  const {
    data: blogpostData,
    isLoading,
    refetch,
    error
  } = useGetBlogpostDetailsQuery(id)

  const [updateBlogpost, { isLoading: loadingUpdate }] = 
    useUpdateBlogpostMutation()

  useEffect(() => {
    if (blogpostData) {
      setValue('image', blogpostData.image)
      setValue('title', blogpostData.title)
      setValue('subtitle', blogpostData.subtitle)
      setValue('content', blogpostData.content)
      setValue('category', blogpostData.category)
    }
  }, [blogpostData])

  const {
    register,
    setValue,
    handleSubmit,
    formState: { errors }
  } = useForm()

  const onFormSubmit = async data => {
    if (data.image[0]) {
      const formData = new FormData()
      formData.append('_id', id)
      formData.append('image', data.image[0])
      data = { ...data, image: data.image[0].name }
      formData.append('image', data.image)
      formData.append('title', data.title)
      formData.append('subtitle', data.subtitle)
      formData.append('content', data.content)
      formData.append('category', data.category)
      try {
        const response = await updateBlogpost(formData).unwrap()
        toast.success('Blogpost has been updated')
        refetch()
      } catch (err) {
        toast.error(err?.data?.message || err.error)
      }
    } else {
      try {
        const response = await updateBlogpost({ ...data, _id: id }).unwrap()
        toast.success('Blogpost has been updated')
        refetch()
      } catch (err) {
        toast.error(err?.data?.message || err.error)
      }
    }
  }

  return (
    <FormContainer>
      {loadingUpdate && <Loader />}

      {isLoading ? (
        <Loader />
      ) : error ? (
        <p>{error.data.message}</p>
      ) : (
        <>
          <img src={blogpostData.image.url} style={{ width: '150px' }} />
          <form onSubmit={handleSubmit(onFormSubmit)}>
            <label htmlFor='image' name='image'>
              image
            </label>
            <input type='file' {...register('image')} />
            <p>{errors.image?.message}</p>
            <label htmlFor='title' name='title'>
              Title
            </label>
            <input type='text' {...register('title')} />
            <p>{errors.title?.message}</p>
            <label htmlFor='subtitle' name='subtitle'>
              Subtitle
            </label>
            <input type='text' {...register('subtitle')} />
            <p>{errors.subtitle?.message}</p>
            <label htmlFor='content' name='content'>
              Content
            </label>
            <textarea
              rows='10'
              cols='100'
              type='text'
              {...register('content')}
            />
            <p>{errors.content?.message}</p>
            <label htmlFor='category'>Choose a category:</label>
            <select name='category' {...register('category')}>
              <option value='' />
              <option value='game'>Game</option>
              <option value='tv'>TV</option>
              <option value='anime'>Anime</option>
              <option value='book'>Book</option>
            </select>
            <p>{errors.category?.message}</p>
            <button type='submit'>Submit</button>
          </form>
        </>
      )}
    </FormContainer>
  )
}

export default BlogpostEdit

Slice/Mutation代码

updateBlogpost: builder.mutation({
      query: (data) => ({
        url: `${BLOGPOSTS_URL}/${data._id}`,
        method: 'PUT',
        body: data,
      }),
      invalidatesTags: ['Blogposts'],
    }),

后端控制器代码

const updateBlogpost = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const blogpost = await Blogpost.findByIdAndUpdate(id, { ...req.body });
  if (req.file) {
    blogpost.image.url = req.file.path;
    blogpost.image.filename = req.file.filename;
  }
  const updatedBlogpost = await blogpost.save();
  if (updatedBlogpost) {
    res.status(200).json(updatedBlogpost);
  } else {
    res.status(404);
    throw new Error('Resouce not found');
  }
});

路由代码

router
  .route('/:id')
  .put(registered, admin, upload.single('image'), updateBlogpost);

错误细节

  • 提交表单时的数据:包含_id、image、title等字段,_id存在有效值
  • FormData内容:包含_id、两次image(文件对象+文件名)、其他表单字段
  • 控制台错误:Cast to objectId failed for value 'undefined' (type string) at path '_id' for model 'Blogpost'
  • try/catch捕获的错误:Resource not found

解决方案

问题根源:FormData是特殊对象,无法通过.直接访问属性,导致Redux Toolkit构造URL时data._id为undefined,最终请求URL变为/blogposts/undefined,后端路由无法匹配:id参数,因此req.params.id为undefined。

修复步骤

  1. 修正Redux Toolkit的Mutation逻辑
    直接使用组件中的id参数构造URL,避免从FormData中读取:

    updateBlogpost: builder.mutation({
      query: ({ id, formData }) => ({
        url: `${BLOGPOSTS_URL}/${id}`,
        method: 'PUT',
        body: formData,
      }),
      invalidatesTags: ['Blogposts'],
    }),
    
  2. 更新前端FormData提交代码
    传递id和formData的组合对象给mutation:

    // 原代码:await updateBlogpost(formData).unwrap()
    // 修改为:
    const response = await updateBlogpost({ id, formData }).unwrap()
    
  3. 优化FormData构造逻辑
    移除重复的image字段添加,且无需将_id加入FormData(已通过URL传递):

    const formData = new FormData()
    formData.append('image', data.image[0])
    formData.append('title', data.title)
    formData.append('subtitle', data.subtitle)
    formData.append('content', data.content)
    formData.append('category', data.category)
    
  4. 后端补充空值校验
    避免id为undefined时执行无效数据库操作:

    const updateBlogpost = asyncHandler(async (req, res) => {
      const { id } = req.params;
      if (!id) {
        res.status(400);
        throw new Error('Blogpost ID is required');
      }
      let blogpost = await Blogpost.findById(id);
      if (!blogpost) {
        res.status(404);
        throw new Error('Resource not found');
      }
      // 逐个更新字段,避免覆盖未修改的内容
      blogpost.title = req.body.title || blogpost.title;
      blogpost.subtitle = req.body.subtitle || blogpost.subtitle;
      blogpost.content = req.body.content || blogpost.content;
      blogpost.category = req.body.category || blogpost.category;
      if (req.file) {
        blogpost.image.url = req.file.path;
        blogpost.image.filename = req.file.filename;
      }
      const updatedBlogpost = await blogpost.save();
      res.status(200).json(updatedBlogpost);
    });
    

为什么文本编辑时正常?

文本编辑时传递的是普通JS对象,带有_id属性,Redux Toolkit能正常读取data._id并构造正确URL;而FormData无法通过.访问属性,导致URL构造错误,后端路由匹配失败。


内容的提问来源于stack exchange,提问作者Vasilis Zisis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 08:32:32