0

I have a react app and Vercel api connected with Stripe. I have set up a webhook where after a successful payment the email should be sent to my gmail with all the information about the order as well as images of the items. It all works very well if I attach up to 2 images. If there are more than 2 images attached the email does not get sent and no error messages are returned.

The webhook code

import { buffer } from "micro";
import Stripe from "stripe";
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { storage } from "../src/utils/firebase.js";
import { ref, getDownloadURL } from "firebase/storage";
import bucket from "../src/utils/firebaseAdmin.js";

export default async function handler(req: VercelRequest, res: VercelResponse) {
  if (req.method === "POST") {
    let event: { [key: string]: any };
    const stripe = new Stripe(      "sk_test...........",
      { apiVersion: "2022-11-15" }
    );
    try {
      const requestBuffer = await buffer(req);
      const signature = req.headers["stripe-signature"] as string;

      event = stripe.webhooks.constructEvent(
        requestBuffer.toString(),
        signature,
        "whsec_................" 
      );
    } catch (error) {
      res.status(400).send(`Webhook error: ${error}`);
      return;
    }

    //remove images from firebase
    const deleteImageFromFirebase = async (user: string) => {
      await bucket.deleteFiles({ prefix: `temp/${user}` });
    };

    const user = event.data.object.metadata.user;

    switch (event.type) {
      case "checkout.session.completed": {
        const sessionWithLineItems = await stripe.checkout.sessions.retrieve(
          event.data.object.id,
          {
            expand: ["line_items"],
          }
        );
        const lineItems = sessionWithLineItems.line_items;
        const purchasedItems = lineItems?.data.map(
          (item: { [key: string]: any }) => {
            return item.description;
          }
        );

        //send email to own email about new order
        let imgUrls: { [key: string]: any }[] = [];
        for (let i = 0; (purchasedItems as string[]).length > i; i++) {
          const myRef = ref(storage, `temp/${user}/${i}.png`);
          const downloadUrl = await getDownloadURL(myRef);
          imgUrls.push({ filename: `${i + 1}.png`, path: downloadUrl });
        }
        const email = event.data.object.customer_details.email;
        const name = event.data.object.customer_details.name;
        const to = "[email protected]";
        const from = `New order from ${name} <.....@....>`;
        const subject = `There is a new order from ${name}`;

        try {
          await fetch("http://localhost:3001/api/submitForm", {
            method: "POST",
            headers: {
              "Content-Type": "application/json;charset=utf-8",
            },
            body: JSON.stringify({
              name: `${name}`,
              email: `${email}`,
              message: `There is a new order:
            ${purchasedItems?.map((item: string, i: number) => {
              return ` ${i + 1}: ${item}`;
            })}
           `,
              attachments: imgUrls,
              to: to,
              from: from,
              subject: subject,
              date: date,
              toCustomer: false,
            }),
          }).then(() => {
            //remove temporary images from firebase
            deleteImageFromFirebase(user);
          });
        } catch (e) {
          console.log(e)
          res.status(500).send(`Webhook error: ${e}`);
        }
        res.status(200);
        break;
      }
      case "checkout.session.expired": {
        //remove temporary images from database
        deleteImageFromFirebase(user);
        break;
      }
    }
  }
}

Nodemailer code

import type { VercelRequest, VercelResponse } from '@vercel/node';
import nodemailer from "nodemailer";

const allowCors =
  (fn: any) => async (req: VercelRequest, res: VercelResponse) => {
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader(
      "Access-Control-Allow-Methods",
      "GET,OPTIONS,PATCH,DELETE,POST,PUT"
    );
    res.setHeader(
      "Access-Control-Allow-Headers",
      "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
    );
    if (req.method === "OPTIONS") {
      res.status(200).end();
      return;
    }
    return await fn(req, res);
  };

async function submitForm(req: VercelRequest, res: VercelResponse) {
  if (req.method === "POST") {
    const name = req.body.name;
    const email = req.body.email;
    const message = req.body.message;
    const to = req.body.to;
    const from = req.body.from;
    const subject = req.body.subject;
    const attachments = req.body.attachments || [];
    const toCustomer = req.body.toCustomer;

    const contactEmail = nodemailer.createTransport({
      host: "smtp.gmail.com",
      port: 465,
      secure: true,
      auth: {
        user: process.env.EMAIL_USER,
        pass: process.env.EMAIL_PASS,
      },
    });

    await new Promise((resolve, reject) => {
      contactEmail.verify((error, success) => {
        if (error) {
          console.log(error);
          reject(error);
          res.status(500).send(`Nodemailer error: ${error}`);
        } else {
          console.log("Ready to Send");
          resolve(success);
        }
      });
    });

    const mailInfo = {
      from: from,
      to: to,
      subject: subject,
      html: toCustomer
        ? `<p>${message}</p>`
        : `<p>Nombre: ${name}</p>
             <p>Correo: ${email}</p>
             <p>Mensaje: ${message}</p>`,
      attachments: attachments,
    };

    await new Promise((resolve, reject) => {
      console.log("inside promise before send")
      contactEmail.sendMail(mailInfo, (error, info) => {
        console.log("inside send");
        if (error) {
          console.log(error)
          reject(error);
          res.status(500).send(`Nodemailer error: ${error}`);
        } else {
          console.log(info)
          resolve(info);
          res.status(200);
        }
      });
    });
    console.log("end")
  }
}

export default allowCors(submitForm);

logs when I am attaching 1 image (850kb - 2.5mb)

Ready to Send
inside promise before send
inside send
{
 ...info
}
end

logs when I am attaching 4 images or more (~800b/img)

Ready to Send
inside promise before send

logs of attachments

[ { filename: '1.png', path: 'firebasestorage.googleapis.com...' }, { filename: '2.png', path: 'firebasestorage.googleapis.com...' }, { filename: '3.png', path: 'firebasestorage.googleapis.com...' }, { filename: '4.png', path: 'firebasestorage.googleapis.com...' } ]

It never reaches "inside send" I was looking through everywhere and tried changing the code without any luck. Any ideas of what can be wrong or could fix it would be appreciated.

6
  • Could this be related to an attachment size limitation with nodemailer? It looks like this was encountered before github.com/nodemailer/nodemailer/issues/400. Could you try logging the size of mailInfo and see if there is a threshold? Commented Aug 10, 2023 at 16:46
  • I do not think so. I have attached 1 image around 800kb and all went through perfectly then I added 4 images around 850b each and it doesnt reach inside send. Commented Aug 10, 2023 at 18:56
  • Just added 2.5mb image and it all went well. It must be something with multiple attachments Commented Aug 10, 2023 at 19:02
  • Can you provide a log of what the attachments variable contains? Feel free to anonymize any PII in the filenames but it might help understanding if the value matches the expected format here: nodemailer.com/message/attachments Commented Aug 10, 2023 at 20:42
  • [ { filename: '1.png', path: 'firebasestorage.googleapis.com...' }, { filename: '2.png', path: 'firebasestorage.googleapis.com...' }, { filename: '3.png', path: 'firebasestorage.googleapis.com...' }, { filename: '4.png', path: 'firebasestorage.googleapis.com...' } ] I believe I have set it up by the documentation Commented Aug 10, 2023 at 22:23

1 Answer 1

0

After checking the logs in Vercel I have noticed that the function timesout if I add more images. The free version for Vercel has an execution duration limit of 10s. I guess the solution to my problem would be to upgrade to pro which would increase the execution duration to 60s or to use other services like firebase with max function execution duration 540s.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.