Empty file uploads to AWS S3 bucket through Next.js and Mailgun integration

I’m working on a contact form using Next.js that uploads files to AWS S3 and sends emails via Mailgun. The form has regular input fields plus file upload functionality. When someone uploads a file, it should go to S3 first, then the S3 URL gets attached to the email. Everything seems to work but the uploaded files are completely empty. The file names are correct in both the S3 bucket and email attachments, but whether it’s an image, PDF, or text file, they all have zero content.

s3Upload.ts (api route)

const awsS3 = new AWS.S3({
  region: process.env.AWS_REGION,
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
  signatureVersion: 'v4',
});

export const config = {
  api: {
    bodyParser: {
      sizeLimit: '10mb',
    },
  },
};

export default async (req, res) => {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const { fileName, fileType } = req.body;

    const uploadParams = {
      Bucket: process.env.S3_BUCKET_NAME,
      Key: fileName,
      Expires: 600,
      ContentType: fileType,
      ACL: 'public-read',
    };

    const signedUrl = await awsS3.getSignedUrlPromise('putObject', uploadParams);

    res.status(200).json({ signedUrl });
  } catch (error) {
    console.log(error);
    res.status(400).json({ error: error.message });
  }
};

File upload handler

const onFileChange = async (e) => {
  e.preventDefault();
  e.stopPropagation();

  const selectedFile = e.target.files[0];
  setCurrentFile(selectedFile);
  setCurrentFileName(selectedFile.name);
  setCurrentFileType(selectedFile.type);
  setS3FileUrl(S3_BUCKET_URL + selectedFile.name);

  const response = await axios.post('/api/s3Upload', {
    fileName: selectedFile.name,
    fileType: selectedFile.type,
  });

  const uploadUrl = response.data.signedUrl;

  await axios.put(uploadUrl, currentFile, {
    headers: {
      'Content-Type': selectedFile.type,
    },
  });

  setCurrentFile(null);
};

Form submission handler

const onFormSubmit = async (event) => {
  event.preventDefault();
  
  const formPayload = {
    senderName: userName,
    emailSubject: userSubject,
    senderEmail: userEmail,
    messageContent: userMessage,
    attachmentUrl: s3FileUrl
  };

  await fetch('/api/sendEmail', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(formPayload),
  });
};

The files appear in S3 with correct names but zero bytes. Any ideas what could be causing this issue?

Your problem is with React state timing. You’re setting currentFile then immediately trying to use it in your axios PUT request, but state updates are async so currentFile doesn’t have the right value yet. Just use selectedFile directly in your axios call instead - it already has the file info from the event. That’ll fix your empty S3 uploads. Also might want to separate your upload logic so state updates don’t mess with each other.

Your file upload handler has a timing issue. You’re using currentFile in the axios PUT request, but currentFile gets set asynchronously - so the PUT happens before the state actually updates. Just use selectedFile instead since you already have the file object from the event target. Also, make sure your S3 upload runs after you get the signed URL response, not at the same time as setting state. That timing mismatch is why you’re getting empty files.

Classic React state issue. You’re setting currentFile then immediately using it in the axios call, but state updates are async. Just use selectedFile directly in your PUT request instead of currentFile - you already have that variable from the event.