通过NextJS API路由上传文件后Symfony的request files为空
文件上传问题修复:NextJS → NextJS API → Symfony
问题根源
- 前端手动设置Content-Type:FormData上传时,浏览器会自动生成带
boundary的multipart/form-data头,手动设置为file.type会破坏这个机制,导致后端无法解析表单。 - NextJS API路由错误传递Content-Type:转发请求给Symfony时,手动复用原请求的Content-Type,没有让axios自动处理FormData的头信息,同样丢失了boundary。
- 文件流未正确读取:formidable解析后的临时文件,直接append到FormData时没有读取文件内容,导致Symfony收到空文件或不完整内容。
修复步骤1:前端代码调整
移除手动设置的Content-Type,让axios自动处理FormData的请求头:
import React, { useState } from 'react'; import Head from "next/head"; import axios from "axios"; import {toast} from "react-toastify"; import Loader from "@/components/Loader"; import {useToken} from "@/contexts/JWTContext"; import {useRouter} from "next/router"; function NewBanner(props) { const {jwt} = useToken(); const router = useRouter(); const {pid} = router.query; const [file, setFile] = useState(null) const [loader, setLoader] = useState(false); const handleSubmit = async (e) => { e.preventDefault() setLoader(true); const asset = new FormData(); asset.append("file", file) try { const r = await axios.post( '/api/assets/banners/new', asset, { headers: { Authorization: `Bearer ${jwt}`, // 移除手动设置的Content-Type,让axios自动处理 }, withCredentials: true } ); toast.success(r.data.message, { position: "top-center", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, theme: "dark", }); router.push('/'); } catch (e) { toast.error(e.response?.data?.message || e.message, { position: "top-center", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, theme: "dark", }); router.push('/'); } finally { setLoader(false); } } return ( <> <Head> <title>Add new picture/video</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> {/* 修正favicon路径,public目录无需显式写入 */} </Head> <main className="pt-12 lg:pt-24"> {loader ? <Loader/> : ( <div className="px-6 lg:px-8"> <h3 className="mb-4 text-xl font-medium text-gray-900 dark:text-white">Spark interest and boost sales: introduce new banners regularly.</h3> <form className="space-y-6" onSubmit={handleSubmit}> <div> <label htmlFor="file" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Attachment</label> <input onChange={(e) => setFile(e.target.files[0])} type="file" name="file" id={`new-file-${props.pid}`} className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required /> </div> <button type="submit" className="w-full text-white bg-teal-800 effect hover:bg-grn focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Done</button> </form> </div> )} </main> </> ); } export default NewBanner;
修复步骤2:NextJS API路由调整
- 移除手动设置的Content-Type,让axios自动处理FormData的请求头
- 使用
fs.createReadStream读取formidable生成的临时文件,确保文件内容完整传递给Symfony
import axios from "axios"; import formidable, { IncomingForm } from "formidable"; import fs from "fs"; export default async function handler(req, res) { try { // 解析前端传来的FormData const asyncParse = (req) => new Promise((resolve, reject) => { const form = new IncomingForm(); form.parse(req, (err, fields, files) => { if (err) return reject(err); resolve({ fields, files }); }); }); const { files } = await asyncParse(req); const bannerFile = files.file[0]; // 构建转发给Symfony的FormData const formData = new FormData(); // 用文件流读取临时文件,确保内容完整 formData.append( "banner_form[bannerFile]", fs.createReadStream(bannerFile.filepath), { filename: bannerFile.originalFilename, contentType: bannerFile.mimetype } ); // 获取token,增加错误处理 const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ message: "Authorization header missing" }); } const token = authHeader.split(' ')[1]; // 转发请求到Symfony,让axios自动处理Content-Type const response = await axios.post( `http://localhost:8000/api/new/banner`, formData, { headers: { Authorization: `Bearer ${token}`, // 移除手动设置的Content-Type,axios会自动添加带boundary的multipart/form-data头 } } ); res.status(200).json(response.data); } catch (e) { console.error("Error forwarding to Symfony:", e); res.status(e.response?.status || 500).json({ message: e.response?.data?.message || "Internal server error" }); } } export const config = { api: { bodyParser: false } };
验证要点
- 前端提交时,查看浏览器网络请求的Content-Type,应该是
multipart/form-data; boundary=----WebKitFormBoundaryxxxx格式 - NextJS API控制台输出的文件信息中,filepath指向的临时文件存在且大小与原文件一致
- Symfony端
$request->files->get('banner_form')['bannerFile']可获取到完整的上传文件
内容的提问来源于stack exchange,提问作者tsukingo
相关产品推荐
相关产品推荐

