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

React移除硬编码端点后使用fetchWrapper遇400错误的解决方法

问题分析与解决

问题背景

原本有一段可正常运行的产品图片上传代码,为移除硬编码API端点封装了fetchWrapper工具函数,但替换后接口返回400错误,提示productId不符合要求——尽管已确认productId是有效非空字符串,API地址和端点也正常。

原可运行代码

import { AnyARecord } from "dns";
import client from "@/lib/fetchWrapper";

export default async function addProductImage({
  photo,
  productId,
}: {
  photo: File;
  productId: string;
}) {
  const formData = new FormData();
  formData.append("productId", productId);
  formData.append("images", photo);
  console.log("==photo==", Object.values(photo));
  const files = Object.values(photo)
  const token =
    localStorage.getItem("ecommerceToken") ||
    sessionStorage.getItem("ecommerceToken");
 
for (const key in files) {
        formData.append('images', files[key as string])
      }
  const response = await fetch("http://localhost:4005/v1/product-image", {
    method: "POST",
    body: formData,
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "multipart/form-data",
    },
  });
}

封装的fetchWrapper函数

export default function client(
  endpoint: string,
  { body, ...customConfig }: any = {}
) {
  const token =
    localStorage.getItem("ecommerceToken") ||
    sessionStorage.getItem("ecommerceToken");
  const headers = { "content-type": "application/json", Authorization: "" };
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }
  const config = {
    method: body ? "POST" : "GET",
    ...customConfig,
    headers: {
      ...headers,
      ...customConfig.headers,
    },
  };
  if (body) {
    config.body = JSON.stringify(body);
  }

  return fetch(`${process.env.NEXT_PUBLIC_API_URL}/${endpoint}`, config).then(
    async (response) => {
      if (response.ok) {
        return await response.json();
      } else {
        const error = await response.json();
        return Promise.reject(new Error(error?.message));
      }
    }
  );
}

替换后的代码

import client from "@/lib/fetchWrapper";

export default async function addProductImage({
  photo,
  productId,
}: {
  photo: File;
  productId: string;
}) {
  const formData = new FormData();
  formData.append("productId", productId);

  Object.values(photo).forEach((img) => formData.append("images", img));

  const response = await client("v1/product-image", {
    method: "POST",
    body: formData,
    headers: {
      'Content-Type': 'multipart/form-data', // 设置Content-Type头
    },
  });

  return response;
}

收到的错误信息

{
    "statusCode": 400,
    "message": [
        "productId must be a string",
        "productId should not be empty"
    ],
    "error": "Bad Request"
}

问题根源

问题出在fetchWrapper函数的两处逻辑:

  1. 默认Content-Type错误:函数默认设置content-type: application/json,但上传FormData时不需要手动指定该头——浏览器会自动生成包含边界信息的multipart/form-data头,手动设置反而会导致后端无法解析表单。
  2. 错误序列化FormData:函数对所有body执行JSON.stringify(body),但FormData对象无法被JSON序列化,这会导致表单数据完全损坏,后端自然读取不到productId。

解决方案

修改fetchWrapper函数,针对FormData类型做特殊处理:

export default function client(
  endpoint: string,
  { body, ...customConfig }: any = {}
) {
  const token =
    localStorage.getItem("ecommerceToken") ||
    sessionStorage.getItem("ecommerceToken");
  
  // 初始化headers,不默认设置content-type,让浏览器自动处理
  const headers: Record<string, string> = { Authorization: "" };
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }

  const config = {
    method: body ? "POST" : "GET",
    ...customConfig,
    headers: {
      ...headers,
      ...customConfig.headers,
    },
  };

  // 仅当body不是FormData时才序列化,FormData直接传递
  if (body) {
    if (!(body instanceof FormData)) {
      config.body = JSON.stringify(body);
      // 非FormData请求添加json头
      config.headers["content-type"] = "application/json";
    }
    // FormData直接使用原对象,不做处理
  }

  return fetch(`${process.env.NEXT_PUBLIC_API_URL}/${endpoint}`, config).then(
    async (response) => {
      if (response.ok) {
        return await response.json();
      } else {
        const error = await response.json();
        return Promise.reject(new Error(error?.message));
      }
    }
  );
}

同时,调用client时移除手动设置的Content-Type头:

const response = await client("v1/product-image", {
  method: "POST",
  body: formData,
  // 去掉手动设置的Content-Type
});

关键说明

  • 上传FormData时,禁止手动设置Content-Type: multipart/form-data,浏览器会自动生成带正确边界的头,确保后端能解析表单字段。
  • FormData不能被JSON.stringify处理,必须直接作为请求体传递。
  • 区分普通JSON请求和FormData请求,分别处理请求头与请求体逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 14:43:30