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

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 = '文件上传失败,请重试'
      }
    }
  }
}

说明

  1. 每个文件对应独立FormData:遍历预签名响应数组,为每个文件创建新的FormData,只添加该文件对应的预签名字段和文件内容,确保每个POST请求仅包含一个file字段。
  2. 批量上传并处理结果:用Promise.all等待所有上传请求完成,统一处理成功或失败状态,同时更新UI提示用户。
  3. 设置正确请求头:明确指定Content-Type为multipart/form-data,确保请求格式符合S3要求。

后端代码无需修改,因为它已经正确为每个文件生成了独立的预签名POST参数。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 06:35:21