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函数的两处逻辑:
- 默认Content-Type错误:函数默认设置
content-type: application/json,但上传FormData时不需要手动指定该头——浏览器会自动生成包含边界信息的multipart/form-data头,手动设置反而会导致后端无法解析表单。 - 错误序列化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
相关产品推荐
相关产品推荐

