XHR/Axios Progress with Server Actions

Hello, I'm looking into a way to use the NextJS 15 Server Actions to upload files to an S3 bucket (which I have implemented and is working); however I am now trying to get the progress of the file upload and use that in a progress bar. I have tried looking into XHR and Axios but have only been able to find the solution for API routes and not Server Actions. My Current Code:
Solution:
I have figured a possible solution? However, I'm unsure if it makes sense to do so or just seems redundant? But I have created an API route to effectively collect the data; then send it to a controller to separate the formData and create different variables for the files and the payload; then I pass that controller data down to the server action. This does seem to be working....
Jump to solution
2 Replies
PuzzledApples
PuzzledApplesOP2mo ago
// app/actions/s3File.ts

"use server"

import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { revalidatePath } from "next/cache"

type S3Payload = {
uploadDestination: string
bucket: string
hostName: string
uploadEndpoint: string
}

export async function uploadFileToS3( formData: FormData, payload: S3Payload ): Promise<any[]> {
const s3 = new S3Client({
...
})

const { uploadDestination, bucket, hostName, uploadEndpoint } = payload

try {
const files = formData.getAll("file") as File[]

const response = await Promise.all(
files.map(async (file) => {
const {name: fileName, type: fileType, lastModified: date, size: fileSize} = file
const fileExtension = fileType.split("/")\[1\]
const fileID = `${uploadDestination}\_CUSTOMRANDOMID` as string
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)
const upload = await s3.send(uploadFile)
})
)

revalidatePath("/")
return response
} catch (error) {
...
}
}
// app/actions/s3File.ts

"use server"

import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { revalidatePath } from "next/cache"

type S3Payload = {
uploadDestination: string
bucket: string
hostName: string
uploadEndpoint: string
}

export async function uploadFileToS3( formData: FormData, payload: S3Payload ): Promise<any[]> {
const s3 = new S3Client({
...
})

const { uploadDestination, bucket, hostName, uploadEndpoint } = payload

try {
const files = formData.getAll("file") as File[]

const response = await Promise.all(
files.map(async (file) => {
const {name: fileName, type: fileType, lastModified: date, size: fileSize} = file
const fileExtension = fileType.split("/")\[1\]
const fileID = `${uploadDestination}\_CUSTOMRANDOMID` as string
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)
const upload = await s3.send(uploadFile)
})
)

revalidatePath("/")
return response
} catch (error) {
...
}
}
and my upload file is currently looking like this:
// uploadPage/component.tsx

...

import { Dropzone, DropzoneProps, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { uploadFileToS3 } from "@/app/actions/s3File";

...

async function handleOnSubmit(e: any) {
setUploading(true)

const files = e
const uploadDestination = mediaType

try {
const formData = new FormData()

files.forEach((file) => formData.append("file", file))
// This is where I think the Axios would come in effect for upload progress?!
await uploadFileToS3(formData, {
uploadDestination, // Picture | Video | Audio | ECT
bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME!,
hostName: process.env.NEXT_PUBLIC_S3_HOST_NAME!,
uploadEndpoint: `https://${process.env.NEXT\_PUBLIC\_S3\_BUCKET\_NAME!}.${process.env.NEXT\_PUBLIC\_S3\_HOST\_NAME!}`
})
} catch (error) {
console.error("File(s) couldn't be uploaded to S3", error)
}
}

return <>
...

<Dropzone
onDrop={(files) => handleOnSubmit(files)}
onReject={(files) => console.log('rejected files', files)}
loading={isUploading}
bg="none"
radius="md"
c="white"
{...props}
>
...
</Dropzone>
<Progress radius="0 0 0 1rem" size="xl" value={uploadProgress} color="primary" mt="0.5rem" animated />
...
</>
// uploadPage/component.tsx

...

import { Dropzone, DropzoneProps, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { uploadFileToS3 } from "@/app/actions/s3File";

...

async function handleOnSubmit(e: any) {
setUploading(true)

const files = e
const uploadDestination = mediaType

try {
const formData = new FormData()

files.forEach((file) => formData.append("file", file))
// This is where I think the Axios would come in effect for upload progress?!
await uploadFileToS3(formData, {
uploadDestination, // Picture | Video | Audio | ECT
bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME!,
hostName: process.env.NEXT_PUBLIC_S3_HOST_NAME!,
uploadEndpoint: `https://${process.env.NEXT\_PUBLIC\_S3\_BUCKET\_NAME!}.${process.env.NEXT\_PUBLIC\_S3\_HOST\_NAME!}`
})
} catch (error) {
console.error("File(s) couldn't be uploaded to S3", error)
}
}

return <>
...

<Dropzone
onDrop={(files) => handleOnSubmit(files)}
onReject={(files) => console.log('rejected files', files)}
loading={isUploading}
bg="none"
radius="md"
c="white"
{...props}
>
...
</Dropzone>
<Progress radius="0 0 0 1rem" size="xl" value={uploadProgress} color="primary" mt="0.5rem" animated />
...
</>
I have also tried to add this to the async function handleOnSubmit(e: any) function
files.forEach((file: string | Blob) => {
formData.append("file", file)
axios
.post(
uploadFileToS3(
formData,
s3Payload
),
{
headers: {
'x-ms-blob-type': 'BlockBlob',
},
maxContentLength: 2e10,
maxBodyLength: 2e10,
onUploadProgress: (event: any) => {
console.log("Hello from event", event)
setUploadProgress(Math.round((event.loaded / event.total) * 100))
}
}
).then((response) => {
console.log(response)
})
.catch((error) => {
if (error.response) {
console.log(error.response)
console.log("server responded")
} else if (error.request) {
console.log("network error")
} else {
console.log(error)
}
})
})
files.forEach((file: string | Blob) => {
formData.append("file", file)
axios
.post(
uploadFileToS3(
formData,
s3Payload
),
{
headers: {
'x-ms-blob-type': 'BlockBlob',
},
maxContentLength: 2e10,
maxBodyLength: 2e10,
onUploadProgress: (event: any) => {
console.log("Hello from event", event)
setUploadProgress(Math.round((event.loaded / event.total) * 100))
}
}
).then((response) => {
console.log(response)
})
.catch((error) => {
if (error.response) {
console.log(error.response)
console.log("server responded")
} else if (error.request) {
console.log("network error")
} else {
console.log(error)
}
})
})
This will upload the content through the Server Action but won't run the rest of the operation such as onUploadProgress or the completed .then. It returns the response error of POST https://DEVSERVER/PAGE/[object%20Promise] 404 (Not Found).
Solution
PuzzledApples
PuzzledApples2mo ago
I have figured a possible solution? However, I'm unsure if it makes sense to do so or just seems redundant? But I have created an API route to effectively collect the data; then send it to a controller to separate the formData and create different variables for the files and the payload; then I pass that controller data down to the server action. This does seem to be working. The API route looks something like this:
// api/upload/route.ts

import { handleFileUpload } from "@/app/controllers/upload.controller";

export async function POST(req: any) {
return handleFileUpload(req)
}
// api/upload/route.ts

import { handleFileUpload } from "@/app/controllers/upload.controller";

export async function POST(req: any) {
return handleFileUpload(req)
}
The controller looks something like this:
// controllers/upload.controller.ts

import { NextResponse } from "next/server"
import { uploadFileToS3 } from "@/app/actions/s3File.ts";

export const handleFileUpload = async (req: any) => {
try {
const form = await req.formData()
const files = form.getAll("files") as any

const payloadRAW = form.get("payload")
const {
uploadDestination,
mediaID,
bucket,
uploadEndpoint,
pathname,
redirectPath
} = JSON.parse(payloadRAW)

const payload = {
uploadDestination,
mediaID,
bucket,
uploadEndpoint,
pathname,
redirectPath
} as any

const uploadURL = await uploadFileToS3(files, payload);
return NextResponse.json({ message: "success", uploadURL });
} catch (error) {
console.error("Error uploading file:", error);
return NextResponse.json({ message: "failure", reason: error.message });
}
}
// controllers/upload.controller.ts

import { NextResponse } from "next/server"
import { uploadFileToS3 } from "@/app/actions/s3File.ts";

export const handleFileUpload = async (req: any) => {
try {
const form = await req.formData()
const files = form.getAll("files") as any

const payloadRAW = form.get("payload")
const {
uploadDestination,
mediaID,
bucket,
uploadEndpoint,
pathname,
redirectPath
} = JSON.parse(payloadRAW)

const payload = {
uploadDestination,
mediaID,
bucket,
uploadEndpoint,
pathname,
redirectPath
} as any

const uploadURL = await uploadFileToS3(files, payload);
return NextResponse.json({ message: "success", uploadURL });
} catch (error) {
console.error("Error uploading file:", error);
return NextResponse.json({ message: "failure", reason: error.message });
}
}
I then reconfigured my server action to take in the Files: File[] instead of the formData: FormData, along with some other general changes.
Want results from more Discord servers?
Add your server