Next.js多文件上传至S3 Bucket报错:POST仅支持单文件请求
问题描述
使用Next.js开发文件上传功能,单文件上传至AWS S3 Bucket正常,但多文件上传时收到S3错误:
return (<Error> <Code>InvalidArgument</Code> <Message>POST requires exactly one file upload per request.</Message> <ArgumentName>file</ArgumentName> <ArgumentValue>2</ArgumentValue> <RequestId>QK93WX2HS53P33TQ</RequestId> <HostId> Wih83YADtKuWKthllaL0jxL5Kh5PH8Xbo1n6QOrOPJH6uNJbB3/QVFEuBT5bhlLMtht9gK25lB4= </HostId> </Error> )
后端代码(uploadurl.ts)
import S3 from 'aws-sdk/clients/s3' import { randomUUID } from 'crypto' import { NextApiResponse } from 'next' import { fileRequestProps } from '../../../types' const { AWS_ACCESS_ID, AWS_SECRET, AWS_BUCKET_NAME, AWS_REGION } = process.env const s3 = new S3({ accessKeyId: AWS_ACCESS_ID, secretAccessKey: AWS_SECRET, signatureVersion: 'v4', region: AWS_REGION }) export default async function handler(req: fileRequestProps, res: NextApiResponse) { const { file }: any = req.query const filesKeyType: fileRequestProps[] = JSON.parse(file || '[]') const uploadToS3 = async (key: string, type: string) => { const params = { Fields: { key: randomUUID() + key, 'Content-Type': type }, Expires: 600, Bucket: AWS_BUCKET_NAME } return new Promise((resolve, reject) => { s3.createPresignedPost(params, (err, signed) => { if (err) return reject(err) resolve(signed) }) }) } if (!file || filesKeyType.length === 0) return res.status(500).json({ message: 'No file uploaded' }) const foodImgUrls = await Promise.all( filesKeyType.map( async ({ key, type }: fileRequestProps) => await uploadToS3(key, type) ) ) res.status(200).json(foodImgUrls) }
前端代码(add.tsx)
import { useState, useEffect, useRef, useContext } from 'react' import Link from 'next/link' import axios from 'axios' import { FileUploadContext } from '../../../contexts/FileUploadContext' import useDocumentTitle from '../../../hooks/useDocumentTitle' import Modal from '../../../components/Modal/Modal' import { Loading } from '../../../components/Icons/Status' import FileUpload from '../../../components/FileUpload' import goTo from '../../../utils/functions/goTo' import scrollToView from '../../../utils/functions/scrollToView' import { API_URL } from '../../../constants' import Layout from '../../../components/dashboard/Layout' import { uploadurlDataProps } from '../../../types' const AddFood = () => { useDocumentTitle('Add Food or Drink') useEffect(() => { scrollToView() setModalLoading(document.querySelector('#modal')!) }, []) //Form States const [addFoodStatus, setAddFoodStatus] = useState() const [modalLoading, setModalLoading] = useState<Element>() //Contexts const { file } = useContext(FileUploadContext) //Form errors messages const ImgErr = useRef<HTMLSpanElement>(null) const handleAddFood = async (e: { target: any key?: string preventDefault: () => void }) => { if (e.key === 'Enter') { //don't submit the form if Enter is pressed e.preventDefault() } else { e.preventDefault() //using FormData to send constructed data const formData = new FormData() // file.map(foodImg => fileData.append('foodImg', foodImg)) const fileData = JSON.stringify( file.map((file: { name: string; type: string }) => { return { key: file?.name, type: file?.type } }) ) const { data }: uploadurlDataProps = await axios.get( `${API_URL}/uploadurl?file=${fileData}` ) data.forEach(({ fields }: any, idx: number) => { Object.entries({ ...fields, file: file[idx] }).forEach(([key, value]) => { formData.append(key, value as string) }) }) async function uploadToS3(url: string) { await axios.post(url, formData) } data.forEach(({ url }: any) => { uploadToS3(url) }) // const ok: { ok: boolean } = await axios.post(url, formData) // console.log(ok) } } return ( <> {addFoodStatus === 1 ? console.log('Added food') : addFoodStatus === 0 && console.error('error adding food')} <Layout> <section className='py-12 my-8 dashboard'> <div className='container mx-auto'> <h3 className='mx-0 mt-4 mb-12 text-2xl text-center md:text-3xl'>ADD FOOD</h3> <div> <div className='food'> {/* Show Modal Loading when submitting form */} <Modal status={Loading} modalHidden='hidden' classes='text-blue-500 text-center' msg='please wait...' /> <form method='POST' className='form' encType='multipart/form-data' onSubmit={e => handleAddFood(e)} > <div className='flex flex-col items-center justify-center gap-4 mb-8 sm:justify-between'> {/* This is custom file input component that return (file) object and saves it to Context */} <FileUpload data={{ defaultImg: [ { foodImgDisplayName: 'food', foodImgDisplayPath: 'https://source.unsplash.com/random?food' } ], foodName: 'Food, Drink, Sweet' }} /> <span className='inline-block md:text-lg text-red-600 dark:text-red-400 font-[600] pt-2 px-1' ref={ImgErr} ></span> </div> <div className='flex items-center justify-evenly'> <button type='submit' className='min-w-[7rem] bg-green-600 hover:bg-green-700 text-white py-1.5 px-6 rounded-md' > Add </button> <Link href={goTo('menu')} className='text-gray-800 underline-hover text-bold dark:text-white' > Menu </Link> </div> </form> </div> </div> </div> </section> </Layout> </> ) } export default AddFood
需求:实现多文件正常上传至S3 Bucket。
解决方案
问题根源
前端代码中,你创建了一个全局的FormData,并将所有文件对应的预签名字段和文件内容都追加进去,随后用同一个FormData向所有预签名URL发起POST请求。这会导致每个请求中包含多个file字段,而AWS S3的预签名POST请求要求每个请求只能上传一个文件,因此触发InvalidArgument错误。
修改前端代码
修改handleAddFood函数,为每个文件单独创建对应的FormData,并单独发起上传请求:
const handleAddFood = async (e: { target: any key?: string preventDefault: () => void }) => { if (e.key === 'Enter') { e.preventDefault() } else { e.preventDefault() const fileData = JSON.stringify( file.map((file: { name: string; type: string }) => { return { key: file?.name, type: file?.type } }) ) const { data }: uploadurlDataProps = await axios.get( `${API_URL}/uploadurl?file=${fileData}` ) // 为每个文件单独创建FormData并上传 const uploadPromises = data.map(({ fields, url }: any, idx: number) => { const formData = new FormData() // 追加预签名字段 Object.entries(fields).forEach(([key, value]) => { formData.append(key, value as string) }) // 追加当前文件 formData.append('file', file[idx]) // 发起上传请求 return axios.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) }) // 等待所有上传完成 try { await Promise.all(uploadPromises) setAddFoodStatus(1) console.log('所有文件上传成功') } catch (err) { setAddFoodStatus(0) console.error('文件上传失败', err) if (ImgErr.current) { ImgErr.current.textContent = '文件上传失败,请重试' } } } }
说明
- 每个文件对应独立FormData:遍历预签名响应数组,为每个文件创建新的
FormData,只添加该文件对应的预签名字段和文件内容,确保每个POST请求仅包含一个file字段。 - 批量上传并处理结果:用
Promise.all等待所有上传请求完成,统一处理成功或失败状态,同时更新UI提示用户。 - 设置正确请求头:明确指定
Content-Type为multipart/form-data,确保请求格式符合S3要求。
后端代码无需修改,因为它已经正确为每个文件生成了独立的预签名POST参数。
内容的提问来源于stack exchange,提问作者Mohammed Ibrahim
相关产品推荐
相关产品推荐

