Search code examples
reactjsformssendgridreact-hook-formsendgrid-api-v3

PDF file upload from react-hook-form won't open with SendGrid API


I have a job application form where users upload their resumes, handled by react-hook-form. I can get the resume saved in state just fine, and I can send it as part of a request to the sendgrid API, but the attachment won't open when you receive it via email.

I've been at this for so long, I have no idea what I'm doing wrong. Any advice is much appreciated. Here's my code so far:

// components/Careers_Application_Form.tsx

import React, { useState } from 'react'
import { useForm } from 'react-hook-form'

export default function Careers_Application_Form() {
     const [file, setFile] = useState(null)
     const [fileName, setFileName] = useState('')

     function handleUploadFile(selectedFile) {
          if (selectedFile.length > 0) {
               let fileToLoad = selectedFile[0]
               setFileName(fileToLoad.name)
               let fileReader = new FileReader()
               fileReader.onload = function (fileLoadedEvent) {
               let file = fileLoadedEvent.target.result
               setFile(file.toString('base64'))
               }
               fileReader.readAsDataURL(fileToLoad)
          }
     }

     const {
          register,
          watch,
          handleSubmit,
          formState: { errors },
     } = useForm({})

     // Handle the submit
     const onSubmit = (data) => {
          handleUploadFile(data.resume)
          SendToSendgrid(data)
          setSubmitted(true)
          console.log(errors)
     }

     // SENDGRID
     async function SendToSendgrid(data) {
          data.file = file
          data.fileName = fileName
          await fetch('/api/sendJobApplication', {
               method: 'POST',
               body: JSON.stringify(data),
          }).then((res) => res.json())
     }

  return (
     <form onSubmit={handleSubmit(onSubmit)}>
          <input
               type='file'
               onChange={setFile}
               placeholder='Upload Your Resume'
               {...register('resume', {
               required: true,
               validate: {
                    lessThan10MB: (files) => files[0]?.size < 10000000 || 'Max 10MB',
                    acceptedFormats: (files) => files[0].type === 'application/pdf',
               },
               })}
          />
          {errors.resume?.type === 'lessThan10MB' && (
               <p>Error: Must Be Under 10MB</p>
          )}
          {errors.resume?.type === 'acceptedFormats' && (
               <p>Error: Must Be A PDF</p>
          )}
          <input
               type='submit'
               value={'off she goes!'}
          />
     </form>
  )
}

Here's the sendgrid API file:

// pages/api/sendJobAppication.ts
import mail from "@sendgrid/mail";
mail.setApiKey(process.env.SENDGRID_API_KEY);

export default async function sendOnboarding(req, res) {
  const body = JSON.parse(req.body);
  await mail.send({
    to: `redacted`,
    from: {
      email: "redacted",
      name: "Pixel Bakery Robot",
    },
    subject: `Job Application: ${body.first_name} ${body.last_name} – ${body.position}`,
    templateId: "redacted",
    dynamicTemplateData: {
     // ...more
    },
    attachments: [
      {
        content: `${body.file}`,
        filename: `${body.fileName}`,
        type: "application/pdf",
        disposition: "attachment",
      },
    ],
  });
  console.log(res.status(200).json({ status: "Ok" }));
}

Here's what happens when I try to open the attachment:

enter image description here


Solution

  • One issue is that you're mixing synchronous and asynchronous logic. Remove the two useState() calls. When you set their values, you won't get the updated values until after the next render. So when handleUploadFile returns and you enter SendToSendgrid the file and fileName values are probably incorrect. I'd have handleUploadFile just return a tuple of [file, filename]. Then on the server-side - you may be passing base64 to SendGrid. They may be expecting binary...

    Check things are multiple points... Some tips that may help you debug:

    1. On the server side, ignore the form submission. Read a temporary pdf file, and attache it. Verify you get that working.
    2. On the server side, get the form submission, save the PDF file to /tmp. Then open it. Is it correct? Same # of bytes as the original?
    3. Dump the data you're sending, and the data you're receiving. Decode it with 3rd party tools. Verify it is what you expect.

    EDIT: It looks like SendGrid does expect base64.

    You can just use the variable directly without pulling them out of a string template:

    attachments: [
      {
        content: `${body.file}`,
        filename: `${body.fileName}`,
        type: "application/pdf",
        disposition: "attachment",
      },
    ],
    
    // change to
    attachments: [
      {
        content: body.file,
        filename: body.fileName,
        type: "application/pdf",
        disposition: "attachment",
      },
    ],