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

首次提交workout表单触发PATCH /workouts/null 404报错排查

问题描述

表单填入数据点击提交按钮,首次提交时触发报错:PATCH http://localhost:5000/workouts/null 404 (Not Found)。首次报错后后续操作可正常运行,但所有首次提交操作都会触发该错误,初步判断为首次提交对象时向数据库传递了无效id。

附涉及的各部分代码:

Workout对象模型定义

import mongoose from "mongoose";


const workoutSchema=mongoose.Schema({
    category:String,
    difficulty:String,
    exercisesNames:[String],
    selectedFile:String,
    likeCount: {
        type:Number,
        default:0
    },
    postedAt:{
        type:Date,
        default:new Date()
    }
});

const WorkoutMessage=mongoose.model('WorkoutMessage',workoutSchema);

export default WorkoutMessage;

后端接口路由代码

import express from 'express';
import mongoose from 'mongoose';
import WorkoutMessage from '../models/workoutMessage.js'

const router = express.Router();

export const getWorkouts=async (req,res)=>{
    try{
        const workoutMessages=await WorkoutMessage.find();
        console.log(workoutMessages);

        res.status(200).json(workoutMessages);
    }catch(error){
        res.status(404).json({message:error.message});
    }
}


export const createWorkout=async (req,res)=>{
    const workout=req.body;

    const newWorkoutMessage=new WorkoutMessage(workout);
    try{
        await newWorkoutMessage.save();

        res.status(201).json(newWorkoutMessage);
    }catch(error)
    {
        res.status(409).json({message:error.message});
    }
}

export const updateWorkout=async(req,res)=>{
    const { id:_id }=req.params;
    const workout=req.body;

    if(!mongoose.Types.ObjectId.isValid(_id)) return res.status(404).send(`No workout with that ${_id}`);

        const updatedWorkout=await WorkoutMessage.findByIdAndUpdate(_id,{...workout,_id},{new:true});

    res.json(updateWorkout);
}

export const deleteWorkout=async(req,res)=>{
    const { id }=req.params;

    if(!mongoose.Types.ObjectId.isValid(id))
    return res.status(404).send('No workout with that id');

    await WorkoutMessage.findByIdAndRemove(id);

    res.json({message:"Workout deleted successfully"});
}

export default router;

客户端action函数代码

import { FETCH_ALL, CREATE, UPDATE, DELETE } from '../constants/actionTypes';

import * as api from '../api';

export const getWorkouts=()=> async (dispatch)=>{
    try{
        const {data}=await api.fetchWorkouts();

        dispatch({type:FETCH_ALL,payload:data});
    }catch(error){
        console.log(error);
    }

}

export const createWorkout=(workout)=>async(dispatch)=>{
    try{
            const {data}=await api.createWorkout(workout);

            dispatch({type:CREATE,payload:data});
            console.log(data);
    }catch(error){
            console.log(error.message);
    }
}

export const updateWorkout=(id,workout)=>async(dispatch)=>{
    try{
        const {data}=await api.updateWorkout(id,workout);

        dispatch({type:UPDATE,payload:data})
    }catch(error){
        console.log(error);
    }
}

export const deleteWorkout=(id)=>async (dispatch)=>{
    try{
        await api.deleteWorkout(id);

        dispatch({type:DELETE,payload:id});
    }catch(error){
        console.log(error);
    }
}

表单Form组件代码

const Form=({ currentId, setCurrentId=()=>{} })=>{
    const [workoutData, setWorkoutData]=useState({
        category:'',difficulty:'', exercisesNames:'',selectedFile:''
    });

    const workout = useSelector((state) => (currentId ? state.workouts.find((workout) => workout._id === currentId) : null));
    const dispatch=useDispatch();


    const classes=useStyles();
  
    useEffect(()=>{
        if(workout) setWorkoutData(workout);
    },[workout]);

    const clear = () => {
        setCurrentId(0);
        setWorkoutData({  category:'',difficulty:'', exercisesNames:'',selectedFile:'' });
      };

   
    const handleSubmit=async (e)=>{
        e.preventDefault();
        if(currentId===0){
            dispatch(createWorkout(workoutData));
            clear();
        } else{
            dispatch(updateWorkout(currentId, workoutData));
            clear();
        }
     
    };
  
    return(
        <Paper className={classes.paper}>
        <form autoComplete="off" noValidate className={`${classes.root} ${classes.form}`} onSubmit={handleSubmit}>
            <Typography variant="h6"></Typography>
            <TextField name="category" variant="outlined" label="Category" fullWidth value={workoutData.category} onChange={(e)=>setWorkoutData({...workoutData, category:e.target.value})}/>
            <TextField name="difficulty" variant="outlined" label="Difficulty" fullWidth value={workoutData.difficulty} onChange={(e)=>setWorkoutData({...workoutData, difficulty:e.target.value})}/>
            <TextField name="exercisesNames" variant="outlined" label="Exercises names" fullWidth value={workoutData.exercisesNames} onChange={(e)=>setWorkoutData({...workoutData, exercisesNames:e.target.value})}/>
            <div className={classes.fileInput}>
                <FileBase
                    type="file"
                    multiple={false}
                    onDone={({base64})=>setWorkoutData({...workoutData,selectedFile:base64})}
                />
            </div>
            <Button className={classes.buttonSubmit} variant="contained" color="primary" size="large" type="submit" fullWidth>Submit</Button>
            <Button variant="contained" color="secondary" size="small" onClick={clear} fullWidth>Clear</Button>
        </form>
        </Paper>
    );
}

export default Form;
根因定位
  • 核心触发原因:Form组件仅给setCurrentId设置了默认空函数,未给currentId属性设置默认值。首次加载时如果父组件未传入currentId,该值为undefined/null,不满足currentId===0的新建逻辑判断条件,错误走到更新分支,携带空id发起PATCH请求,最终请求路径变为/workouts/null触发404。首次提交后clear方法将currentId设为0,后续提交判断逻辑正常,因此不会再触发该错误。
  • 附带隐藏bug:后端updateWorkout接口返回响应时,变量名拼写错误,将存储更新结果的updatedWorkout错写为函数名updateWorkout,会导致更新操作执行成功但接口返回500错误。
修复步骤

1. 修正Form组件的props默认值与提交判断逻辑

给currentId设置默认值0,同时将严格相等判断改为falsy判断,兼容空值场景:

// 原代码
// const Form=({ currentId, setCurrentId=()=>{} })=>{
// 修改为
const Form=({ currentId = 0, setCurrentId=()=>{} })=>{

  // 省略其他代码

  const handleSubmit=async (e)=>{
      e.preventDefault();
      // 原判断 if(currentId===0) 改为
      if(!currentId){
          dispatch(createWorkout(workoutData));
          clear();
      } else{
          dispatch(updateWorkout(currentId, workoutData));
          clear();
      }
  };

同时确认父组件中控制currentId的state初始值设为0,参考写法:

const [currentId, setCurrentId] = useState(0);
// 渲染Form时传入
<Form currentId={currentId} setCurrentId={setCurrentId} />

2. 修复后端updateWorkout接口的变量拼写错误

export const updateWorkout=async(req,res)=>{
    const { id:_id }=req.params;
    const workout=req.body;

    if(!mongoose.Types.ObjectId.isValid(_id)) return res.status(404).send(`No workout with that ${_id}`);

    const updatedWorkout=await WorkoutMessage.findByIdAndUpdate(_id,{...workout,_id},{new:true});
    // 原代码错写为res.json(updateWorkout),修改为
    res.json(updatedWorkout);
}

内容的提问来源于stack exchange,提问作者Alexandra DDT

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 18:24:35