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

NextJS 15中Axios调用Server Action实现文件上传进度展示问题

问题分析与解决方案

核心错误原因

你当前的代码存在两个关键问题:

  1. Axios请求路径错误:你把Server Action函数uploadFileToS3直接传给了Axios的post方法第一个参数,而Axios期望的是请求URL。uploadFileToS3是异步函数,调用后返回Promise对象,被转成字符串[object Promise]作为请求路径,因此出现POST https://DEVSERVER/PAGE/[object%20Promise] 404错误。
  2. 进度监控逻辑错位:原Server Action是在服务器端完成文件到S3的上传,客户端仅负责把文件发送给Server Action——此时你能监控的是客户端到服务器的上传进度,而非服务器到S3的真实上传进度,且原写法无法触发Axios的onUploadProgress回调。

解决方案一:用Axios调用API路由监控客户端到服务器进度

将Server Action逻辑迁移到API路由,用Axios正常调用并监控上传进度:

1. 创建API路由(替代Server Action)

// app/api/upload-to-s3/route.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { revalidatePath } from "next/cache"
import { NextResponse } from "next/server"

type S3Payload = {
  uploadDestination: string
  bucket: string
}

export async function POST(request: Request) {
  try {
    const formData = await request.formData()
    const payload = JSON.parse(formData.get("payload") as string) as S3Payload
    
    const s3 = new S3Client({
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
      },
      region: process.env.S3_REGION!,
    })
    
    const { uploadDestination, bucket } = payload
    const files = formData.getAll("file") as File[]
    
    const response = await Promise.all(
      files.map(async (file) => {
        const { type: fileType, size: fileSize } = file
        const fileExtension = fileType.split("/")[1]
        const fileID = `${uploadDestination}_${Date.now()}`
        const filePath = `${uploadDestination}/${fileID}.${fileExtension}`
        
        const arrayBuffer = await file.arrayBuffer()
        const buffer = Buffer.from(arrayBuffer)
        
        const uploadFileObject = {
          Bucket: bucket,
          Key: filePath,
          Body: buffer,
          ContentType: fileType,
          ContentLength: fileSize,
        }
        
        const uploadFile = new PutObjectCommand(uploadFileObject)
        return await s3.send(uploadFile)
      })
    )
    
    revalidatePath("/")
    return NextResponse.json({ success: true, data: response })
  } catch (error) {
    console.error(error)
    return NextResponse.json({ success: false, error: "上传失败" }, { status: 500 })
  }
}

2. 客户端组件修改

// uploadPage/component.tsx
import { useState } from 'react';
import { Dropzone } from '@mantine/dropzone';
import axios from 'axios';

const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setUploading] = useState(false);

async function handleOnSubmit(files: File[]) {
  setUploading(true);
  setUploadProgress(0);
  
  const uploadDestination = mediaType;
  const s3Payload = {
    uploadDestination,
    bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME!,
  };

  try {
    const formData = new FormData();
    files.forEach((file) => formData.append("file", file));
    formData.append("payload", JSON.stringify(s3Payload));

    await axios.post("/api/upload-to-s3", formData, {
      headers: { "Content-Type": "multipart/form-data" },
      maxContentLength: 2e10,
      maxBodyLength: 2e10,
      onUploadProgress: (event) => {
        if (event.total) {
          const progress = Math.round((event.loaded / event.total) * 100);
          setUploadProgress(progress);
        }
      },
    });
  } catch (error) {
    console.error("文件上传失败", error);
  } finally {
    setUploading(false);
    setUploadProgress(0);
  }
}

// 保留原Dropzone渲染代码

解决方案二:客户端直接用预签名URL上传到S3(推荐)

这种方式让客户端直接和S3交互,能监控真实的S3上传进度,同时减轻服务器压力:

1. 创建预签名URL生成API

// app/api/get-presigned-url/route.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  try {
    const { fileType, uploadDestination, bucket } = await request.json();
    
    const s3 = new S3Client({
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
      },
      region: process.env.S3_REGION!,
    });

    const fileExtension = fileType.split("/")[1];
    const fileID = `${uploadDestination}_${Date.now()}`;
    const filePath = `${uploadDestination}/${fileID}.${fileExtension}`;

    const command = new PutObjectCommand({
      Bucket: bucket,
      Key: filePath,
      ContentType: fileType,
    });

    const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });

    return NextResponse.json({ success: true, signedUrl });
  } catch (error) {
    console.error(error);
    return NextResponse.json({ success: false, error: "生成预签名URL失败" }, { status: 500 });
  }
}

2. 客户端组件修改

// uploadPage/component.tsx
import { useState } from 'react';
import { Dropzone } from '@mantine/dropzone';
import axios from 'axios';

const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setUploading] = useState(false);

async function handleOnSubmit(files: File[]) {
  setUploading(true);
  setUploadProgress(0);
  
  const uploadDestination = mediaType;
  const bucket = process.env.NEXT_PUBLIC_S3_BUCKET_NAME!;

  try {
    await Promise.all(files.map(async (file, index) => {
      // 获取预签名URL
      const presignedRes = await axios.post("/api/get-presigned-url", {
        fileType: file.type,
        uploadDestination,
        bucket,
      });
      const { signedUrl } = presignedRes.data;

      // 上传到S3并监控进度
      await axios.put(signedUrl, file, {
        headers: { "Content-Type": file.type },
        onUploadProgress: (event) => {
          if (event.total) {
            const fileProgress = Math.round((event.loaded / event.total) * 100);
            // 计算总进度(多文件平均分配)
            const totalProgress = Math.round((index * 100 + fileProgress) / files.length);
            setUploadProgress(totalProgress);
          }
        },
      });
    }));

    // 上传完成后刷新页面数据
    await fetch("/api/revalidate", { method: "POST" });
  } catch (error) {
    console.error("文件上传失败", error);
  } finally {
    setUploading(false);
    setUploadProgress(0);
  }
}

// 可选:创建刷新缓存的API
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { NextResponse } from "next/server";

export async function POST() {
  revalidatePath("/");
  return NextResponse.json({ success: true });
}

// 保留原Dropzone渲染代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:44:58