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

React+TypeScript+Next.js表单文件提交客户端报错排查

问题排查:编辑表单图片上传客户端提前报错

问题背景

开发活动编辑表单模态组件时,其他字段可正常更新服务器,但上传新图片时请求未到达服务器就报错,try/catch捕获到的错误为undefined。创建活动的表单使用几乎相同的逻辑可正常上传图片,服务端通过sharp+S3处理文件并保存URL字符串。

相关代码

图片上传上传输入组件

<div className="mb-4">
  <label className="block text-gray-700 text-sm font-bold mb-2">Image Upload</label>
  <input type="file" 
         name="image_url" 
         accept="images/*" 
         key={inputKey} 
         onChange={handleFileChange} 
         className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
    {error && <div style={{color: 'red'}}>{error}</div>}
</div>

handleFileChange 方法

const handleFileChange = (event: any) => {
    const files = event.target.files;

    if (files) {
      const uploadedFile = files[0];
      const fileExtention = uploadedFile.name.split(".").pop()?.toLocaleLowerCase();

      if (fileExtention === 'heic' || fileExtention === 'heif') {
        setInputKey(Date.now());
        setError('HEIC/HEIF images are not supported. Please upload a different file.')
        setFile(null)
      } else {
        setError(null)
        setFile(uploadedFile)
      }
    }
  }

clientAction 方法

const clientAction = async (formData: FormData) => {
    let formDate = formData.get("date") as string;
    const newDate = formDate.length > 1 ? new Date(formData.get("date") as unknown as Date) : eventData.date
    const newImg = file as File;

    const img = newImg && newImg.size ? newImg : formValues["image_url"]

    const updatedEvent = {
        title: formData.get("title") as string,
        date: newDate,
        doors_open: newDoorsOpenTime !== null ? newDoorsOpenTime : formData.get("doors_open") as string,
        start_time:  newStartTime !== null ? newStartTime : formData.get("start_time") as string,
        description: formData.get("description") as string,
        venue_name: formData.get("venue_name") as string,
        address: formData.get("address") as string,
        image_url: img,
        ticket_link: formData.get("ticket_link") as string,
        ticket_price: formData.get("ticket_price") as string,
    }

    try {
      const response = await updateCurrentUserEvent(updatedEvent, eventData.id)
      if (response?.error) {
        console.log("ERROR!!!!")
        toast.error(response.error)
      }
    } catch (error) {
      console.error("ERROR!", error)
    }
  } 

updateCurrentUserEvent 服务端方法

export async function updateCurrentUserEvent(eventData: any, eventId: number) {
  const currentEvent = await prisma.event.findUnique({
    where: { id: eventId }
  })

  let image_url: string | File | null = null
  let date; 

  // if there is no image uploaded
  // use the oringal image_url
  if (!(eventData["image_url"] as File).size) {
    if (currentEvent?.image_url) {
      image_url = currentEvent.image_url
    }
  } else {
    image_url = eventData["image_url"] as File
  }

  // if there is no date chosen,
  // use the original date
  if (!(eventData["date"])) {
    date = currentEvent?.date;
  }

  const convertedStartTime = to24HourFormat(eventData["start_time"] as string)
  const convertedDoorsOpen = to24HourFormat(eventData["doors_open"] as string)
  
  const newEvent = {
    title: eventData["title"] as string,
    date: date ? date : new Date(eventData["date"] as unknown as Date),
    doors_open: convertedDoorsOpen,
    start_time: convertedStartTime,
    description: eventData["description"] as string,
    venue_name: eventData["venue_name"] as string,
    address: eventData["address"] as string,
    image_url,
    ticket_link: eventData["ticket_link"] as string,
    ticket_price: eventData["ticket_price"] as string,
  }

  // valitdate eventData to find errors
  const result = EventSchema.safeParse(newEvent);

  if (!result.success) {
    let errorMessage = "";

    result.error.issues.forEach((issue) => {
      errorMessage = errorMessage + "•" + issue.message + "\n";
    })
    return {
      error: errorMessage
    }
  }

  // if data passed validation, take the uploaded file and 
  // upload to S3 to get url.
  try {
    const file = result.data["image_url"]
    let newImage_url;
    if (file instanceof File) {
    const buffer = Buffer.from(await file.arrayBuffer());
    newImage_url = await uploadFileToS3(buffer, file.name)
    } else {
      newImage_url = currentEvent?.image_url;
    }

    const newData = { ...result.data }

    await prisma.event.update({
      where: {
        id: eventId
      },
      data: {
        ...newData,
        image_url: newImage_url,
      }
    })
  } catch (error) {
    console.error("ERROR", error);
    return { error: "The was an error Updating your event"}
  }
}

控制台错误信息

ERROR! undefined 
    app-index.js:33:21
    error app-index.js:33
    error hydration-error-info.js:45
    overrideMethod myshows:13441
    clientAction EditEventModal.tsx:116
    startHostTransition react-dom.development.js:12693
    startTransition react-dom.development.js:12590
    startHostTransition react-dom.development.js:12690
    submitForm react-dom.development.js:29444
    callCallback react-dom.development.js:20498
    invokeGuardedCallbackImpl react-dom.development.js:20547
    invokeGuardedCallback react-dom.development.js:20622
    invokeGuardedCallbackAndCatchFirstError react-dom.development.js:20636
    executeDispatch react-dom.development.js:31986
    processDispatchQueueItemsInOrder react-dom.development.js:32018
    processDispatchQueue react-dom.development.js:32031
    dispatchEventsForPlugins react-dom.development.js:32042
    dispatchEventForPluginEventSystem react-dom.development.js:32232
    batchedUpdates$1 react-dom.development.js:24843
    batchedUpdates react-dom.development.js:28703
    dispatchEventForPluginEventSystem react-dom.development.js:32231
    dispatchEvent react-dom.development.js:29999
    dispatchDiscreteEvent react-dom.development.js:29970
    (Async: EventListener.handleEvent)
    addEventBubbleListener react-dom.development.js:30219
    addTrappedEventListener react-dom.development.js:32135
    listenToNativeEvent react-dom.development.js:32074
    listenToAllSupportedEvents react-dom.development.js:32085
    listenToAllSupportedEvents react-dom.development.js:32080
    hydrateRoot$1 react-dom.development.js:37768
    hydrateRoot react-dom.development.js:38354
    hydrateRoot client.js:20
    hydrate app-index.js:248
    startTransition react.development.js:2661
    hydrate app-index.js:248
    <anonymous> app-next-dev.js:10
    appBootstrap app-bootstrap.js:57
    loadScriptsInSequence app-bootstrap.js:23
    appBootstrap app-bootstrap.js:56
    <anonymous> app-next-dev.js:8
    NextJS 7

错误截图

问题排查与修复方案

核心问题:File对象序列化失败

编辑表单中,你将File对象直接塞进了普通JS对象updatedEvent中传递给服务端,但Next.js的Client/Server Action无法直接序列化File类型,导致请求在客户端就抛出错误。创建表单能正常工作,大概率是直接传递FormData而非普通对象。

修复步骤

  1. 修改clientAction,用FormData包装所有数据
const clientAction = async (formData: FormData) => {
  // 构建新的FormData,整合原有字段和上传文件
  const submitData = new FormData();
  // 复制原表单所有字段
  for (const [key, value] of formData.entries()) {
    submitData.append(key, value);
  }
  // 添加上传的文件(如果存在)
  if (file) {
    submitData.append('image_url', file);
  }

  try {
    const response = await updateCurrentUserEvent(submitData, eventData.id);
    if (response?.error) {
      toast.error(response.error);
    }
  } catch (error) {
    console.error("ERROR!", error);
  }
}
  1. 调整服务端方法,从FormData中读取数据
export async function updateCurrentUserEvent(formData: FormData, eventId: number) {
  const currentEvent = await prisma.event.findUnique({ where: { id: eventId } });

  // 从FormData中获取字段值
  const title = formData.get('title') as string;
  const dateStr = formData.get('date') as string;
  const date = dateStr.length > 1 ? new Date(dateStr) : currentEvent?.date;
  const doors_open = formData.get('doors_open') as string;
  const start_time = formData.get('start_time') as string;
  const description = formData.get('description') as string;
  const venue_name = formData.get('venue_name') as string;
  const address = formData.get('address') as string;
  const ticket_link = formData.get('ticket_link') as string;
  const ticket_price = formData.get('ticket_price') as string;
  
  // 获取上传文件(可能为null)
  const imageFile = formData.get('image_url') as File | null;

  // 处理图片逻辑
  let image_url: string | null = currentEvent?.image_url;
  if (imageFile instanceof File && imageFile.size > 0) {
    // 保留原S3上传逻辑
    const buffer = Buffer.from(await imageFile.arrayBuffer());
    image_url = await uploadFileToS3(buffer, imageFile.name);
  }

  // 时间格式转换
  const convertedStartTime = to24HourFormat(start_time);
  const convertedDoorsOpen = to24HourFormat(doors_open);

  // 构建待验证的事件对象
  const newEvent = {
    title,
    date,
    doors_open: convertedDoorsOpen,
    start_time: convertedStartTime,
    description,
    venue_name,
    address,
    image_url,
    ticket_link,
    ticket_price,
  };

  // 验证逻辑不变
  const result = EventSchema.safeParse(newEvent);
  if (!result.success) {
    const errorMessage = result.error.issues.map(issue => `•${issue.message}`).join("\n");
    return { error: errorMessage };
  }

  // 更新数据库
  try {
    await prisma.event.update({
      where: { id: eventId },
      data: result.data,
    });
  } catch (error) {
    console.error("ERROR", error);
    return { error: "There was an error updating your event" };
  }
}

其他排查点

  • 检查EventSchema是否对image_url的类型做了限制,确保兼容字符串(URL)和File对象的场景
  • 错误栈中出现hydration-error-info.js,可临时移除inputKey的动态更新,排查是否存在 hydration 不匹配问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 22:12:05