Home > Back-end >  Keep getting the 'cannot set headers' with Axios on Firebase Functions which still fully e
Keep getting the 'cannot set headers' with Axios on Firebase Functions which still fully e

Time:08-02

I have a Firebase function that executes on a Stripe webhooks via express. The function executes fine but the sending of the email (using Axios) keeps resulting in an error: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

It does work fine on localhost but the error appears when pushed to Firebase staging server. Weirdly the whole function executes fully including the Axios call where I'm getting the header issue (sending the email). It does take about 2-3 minutes to fully execute due to the error.

I've tried a number of different methods using return, and then() promises but it's still flagging this error. My code is as follows:

index.js

// Controllers
const stripeWebhookSubscription = require("./src/controllers/stripe/webhooks/subscription");

// Firebase
const admin = require("firebase-admin");
const functions = require("firebase-functions");

// Express
const express = require("express");
const cors = require("cors");

// Stripe
const stripe = require("stripe")(functions.config().stripe.key_secret);

const serviceAccount = require(functions.config().project.service_account);

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: functions.config().project.database_url,
    storageBucket: functions.config().project.storage_bucket,
});

const database = admin.firestore();

// -------------------------
// Stripe
// -------------------------
const stripeFunction = express();
stripeFunction.use(cors({origin: true}));

stripeFunction.post("/webhooks", express.raw({type: "application/json"}), (req, res) => {
    const sig = req.headers["stripe-signature"];

    let event;

    try {
        event = stripe.webhooks.constructEvent(
            req.rawBody,
            sig,
            functions.config().stripe.webhook_secret
        );
    } catch (err) {
        return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    switch (event.type) {
        case "customer.subscription.created":
            stripeWebhookSubscription.createSubscription(database, event.data.object, res);
            break;
        (...)
        default:
            console.log(`Unhandled event type ${event.type}`);
            break;
    }

    res.json({received: true});
});

exports.stripe = functions.https.onRequest(stripeFunction);

subscription.js

const functions = require("firebase-functions");
const stripe = require("stripe")(functions.config().stripe.key_secret);
const axios = require("axios");

class stripeWebhookSubscription {
  static createSubscription(database, subscription, res) {
      let barcode;
      let plan;
      let merchant;
      let customer;

      database.collection("subscriptions").add({
            id: subscription.id,
            customer: subscription.customer,
            status: subscription.status,
            price: {
                amount: (subscription.items.data[0].price.unit_amount / 100).toFixed(2),
                interval: subscription.items.data[0].price.recurring.interval,
                interval_count: subscription.items.data[0].price.recurring.interval_count,
            },
            product: subscription.items.data[0].price.product,
            created: subscription.created,
            current_period_start: subscription.current_period_start,
            current_period_end: subscription.current_period_end,
            cancel_at: subscription.cancel_at,
            cancel_at_period_end: subscription.cancel_at_period_end,
            payment_gateway: "stripe",
            current_usage: 1,
        })
          .then((doc) => {
              barcode = doc.id;
              return database.collection("plans").where("stripe.product", "==", subscription.items.data[0].price.product).limit(1).get();
          })
          .then((docs) => {
              docs.forEach((doc) => {
                  return plan = doc.data();
              });
          })
          .then(() => {
              return database.collection("subscriptions").doc(barcode).set({
                  merchant: plan.merchant,
              }, {merge: true});
          })
          .then(() => {
              return database.collection("merchants").doc(plan.merchant).get();
          })
          .then((doc) => {
              return merchant = doc.data();
          })
          .then(() => {
              async function stripeCustomer() {
                  const stripeData = await stripe.customers.retrieve(subscription.customer);
                  customer = stripeData;
              }
              return stripeCustomer().then(() => {
                  return customer;
              });
          })
          .then(() => {
              return database.collection("customers").doc(subscription.customer).set({
                  name: customer.name,
                  email: customer.email,
                  phone: customer.phone,
                  delinquent: customer.delinquent,
                  created: customer.created,
                  livemode: customer.livemode,
                  merchant: plan.merchant,
                  subscriptions: [barcode],
              }, {merge: true});
          })
          .then((doc) => {
              return axios.request({
                  url: "https://api.sendinblue.com/v3/smtp/email",
                  method: "post",
                  headers: {
                      "api-key": functions.config().sendinblue.key,
                      "Content-Type": "application/json",
                  },
                  data: {
                      "to": [
                          {
                              "email": customer.email,
                              "name": customer.name,
                          },
                      ],
                      "replyTo": {
                          "email": "[email protected]",
                          "name": "Scanable",
                      },
                      "templateId": 2,
                      "params": {
                          "plan_name": plan.name,
                          "interval_count": plan.interval_count,
                          "interval": plan.interval,
                          "subscription": barcode,
                          "merchant_name": merchant.name,
                          "merchant_email": merchant.email,
                      },
                  },
              })
                  .then((response) => {
                      return console.log("Membership email sent to "   customer.email);
                  });
          })
          .then(() => {
              res.status(200).send("✅ Subscription "   subscription.id   " created!");
          })
          .catch((err) => {
              res.status(400).send("⚠️ Error creating subscription ("   subscription.id   "): "   err);
          });
  }
}
module.exports = stripeWebhookSubscription;

CodePudding user response:

In index.js, you call this line:

stripeWebhookSubscription.createSubscription(database, event.data.object, res);

immediately followed by this line:

res.json({received: true});

By the time the createSubscription path has finished, the response has already been sent. When deployed to Cloud Functions, this will also terminate your function before it's done any of its workload (Note: this termination behaviour is not simulated by the local functions emulator).

Depending on what you are trying to achieve, you can probably just add the missing return to this line so that the res.json({received: true}) never gets called:

return stripeWebhookSubscription.createSubscription(database, event.data.object, res);

Additionally, on this line in subscription.js:

database.collection("subscriptions").add({

you need to add the missing return statement so the asynchronous tasks are properly chained:

return database.collection("subscriptions").add({
  • Related