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。
修复步骤
修正Redux Toolkit的Mutation逻辑
直接使用组件中的id参数构造URL,避免从FormData中读取:updateBlogpost: builder.mutation({ query: ({ id, formData }) => ({ url: `${BLOGPOSTS_URL}/${id}`, method: 'PUT', body: formData, }), invalidatesTags: ['Blogposts'], }),更新前端FormData提交代码
传递id和formData的组合对象给mutation:// 原代码:await updateBlogpost(formData).unwrap() // 修改为: const response = await updateBlogpost({ id, formData }).unwrap()优化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)后端补充空值校验
避免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

