Files uploaded to AWS S3 are blank when using Next.js with Mailgun for a contact form

I’m creating a contact form with Next.js that works with AWS S3 for file uploads and Mailgun for email notifications. The form includes standard input fields along with an option to upload files.

The intended process is: a user picks a file → it uploads to S3 → the generated S3 link is included in an email → the email is sent through Mailgun.

However, I’m facing a problem where the uploaded files are showing up as empty in the S3 bucket. Although the files’ names seem correct and I receive emails with those names, all files appear blank regardless of their type (like images or documents).

uploadFile.ts (API route)

const awsS3 = new 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 (request: NextApiRequest, response: NextApiResponse) => {
  if (request.method !== 'POST') {
    return response.status(405).json({ error: 'Method not allowed' });
  }

  try {
    let { fileName, fileType } = request.body;

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

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

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

File handling function

const onFileSelect = async (e: any) => {
  e.preventDefault();
  e.stopPropagation();

  setSelectedFile(e.target.files[0]);
  setFileName(e.target.files[0].name);
  setFileType(e.target.files[0].type);
  setS3FileUrl(BUCKET_BASE_URL + e.target.files[0].name);

  let { data } = await axios.post('/api/fileUpload', {
    fileName: e.target.files[0].name,
    fileType: e.target.files[0].type,
  });

  const uploadUrl = data.signedUrl;

  await axios.put(uploadUrl, selectedFile, {
    headers: {
      'Content-Type': e.target.files[0].type,
    },
  });

  setSelectedFile(null);
};

Form submit function

const onFormSubmit = async (event: any) => {
  event.preventDefault();
  
  const payload = {
    userEmail: email,
    userName: fullName,
    messageSubject: subject,
    messageBody: messageText,
    attachmentUrl: s3FileUrl
  };

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

It looks like the files are actually being uploaded to S3, but they end up being empty. I’m trying to figure out what might be causing this problem.

Had this exact issue a few months ago. Check your S3 bucket config first - ACL settings mess with uploads, especially with public access blocks turned on. Try dropping the ACL parameter from uploadParams and see what happens. You should also wrap that axios.put call in proper error handling. S3 might be throwing errors you’re not seeing. My blank files were from silent upload failures - CORS policy was blocking everything, but the promise wasn’t rejecting so I had no clue it was broken.

The problem’s your timing. You’re setting selectedFile in onFileSelect then immediately trying to use it in the axios.put request, but the state hasn’t actually updated yet. React state updates are async, so selectedFile is probably still null when you hit S3. Skip the state variable and grab the file straight from the event: await axios.put(uploadUrl, e.target.files[0], { headers: { 'Content-Type': e.target.files[0].type } }); I ran into this exact same issue last year. Files would upload with the right names but zero bytes because no actual file data was getting sent. Using the file object directly from the input event fixed it completely.

Check your bodyParser config - you’ve got it set to 10mb but that’s only for json/text data, not multipart files. It might corrupt the file data before reaching your handler. Try disabling bodyParser entirely for this route since you’re handling raw uploads.

Yeah, everyone’s talking about React state, but you’re overcomplicating this. I’ve built tons of contact forms and this pattern always breaks.

You’re juggling three systems - Next.js, AWS S3, and Mailgun. Each has its own issues and failure points. Plus presigned URLs, state management, and file handling all at once.

I switched to Latenode after wasting hours on similar upload bugs. Set up the whole workflow in one spot - file upload, email sending, validation. No more coordinating APIs or fighting state timing.

The automation handles file upload to email notification. Send form data to one webhook and it processes everything. Way cleaner than managing S3 credentials and Mailgun configs in your code.

10 minutes to set up, never worry about blank files or broken uploads again.

Found the bug in your code. Your selectedFile state hasn’t updated when you try using it in the PUT request, but there’s another issue too. You’re building your S3 URL wrong. Setting s3FileUrl to BUCKET_BASE_URL + filename before the upload happens creates a mismatch. The signed URL from S3 has query parameters and a different path structure than your simple string concatenation. I’ve seen this make uploads look successful but write to the wrong spot or fail silently. Don’t build the URL yourself - grab it from the upload response or use the Key from your upload parameters after confirming it worked.

Yeah, definitely a state timing issue like the others mentioned. But also check your Content-Type header - you’re using e.target.files[0].type in the PUT request, which might not match what you sent for the signed URL. Use the same fileType variable for both the signed URL generation and the actual upload to keep them consistent.