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

通过NextJS API路由上传文件后Symfony的request files为空

文件上传问题修复:NextJS → NextJS API → Symfony

问题根源

  1. 前端手动设置Content-Type:FormData上传时,浏览器会自动生成带boundary的multipart/form-data头,手动设置为file.type会破坏这个机制,导致后端无法解析表单。
  2. NextJS API路由错误传递Content-Type:转发请求给Symfony时,手动复用原请求的Content-Type,没有让axios自动处理FormData的头信息,同样丢失了boundary。
  3. 文件流未正确读取: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路由调整

  1. 移除手动设置的Content-Type,让axios自动处理FormData的请求头
  2. 使用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
    }
};

验证要点

  1. 前端提交时,查看浏览器网络请求的Content-Type,应该是multipart/form-data; boundary=----WebKitFormBoundaryxxxx格式
  2. NextJS API控制台输出的文件信息中,filepath指向的临时文件存在且大小与原文件一致
  3. Symfony端$request->files->get('banner_form')['bannerFile']可获取到完整的上传文件

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 14:30:09