Serverless function behaves differently in production and development

PLEASE help us help you by writing a good post!

  • we need to know your netlify site name. Example: kaselartnextjsdev.app.netlify.com

I am building a website for a friend to sell his artwork. I have been trying to get over this problem for a couple weeks now and it is time to task for help!

Context: When a purchase is completed through Stripe Checkout, I have a Stripe Webhook set up to make a call to a Netlify serverless function which in turn calls the Airtable API to decrement the inventory of the product purchased by 1.

The webhook sends an event with the id associated with the product in Airtable. The serverless function then finds the airtable record with that id and then updates the record, decrementing it by 1.

At first, this functionality seemed to work in production, but not 100% of the time. I then tested in development with the Stripe CLI and Netlify CLI. Triggering the webhook there works and I always see the inventory decrease immediately in Airtable.

Problem: In production, I can see that the flow is working as expected until the point when the update request is sent to Airtable. It seems at this point the code stops executing. Again, this does not happen in development in the CLI tools.

Code: I will add the serverless function responsible for receiving the webhook information as well as the development logs (CLI) and production logs (Netlify App) to show where the code stops running.

NETLIFY SERVERLESS FUNCTION

const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
const Airtable = require("airtable");

Airtable.configure({
  endpointUrl: "https://api.airtable.com",
  apiKey: process.env.AIRTABLE_API_KEY,
});

const base = Airtable.base(process.env.AIRTABLE_BASE_ID);
const table = base(process.env.AIRTABLE_TABLE_NAME);

const decrementInventory = (record, newInventory) => {
  console.log("IN DECREMENT FUNCTION");
  console.log(record, newInventory);
  table.update(
    [
      {
        id: record,
        fields: {
          inventory: newInventory,
        },
      },
    ],
    function (err, records) {
      if (err) {
        console.log("ERROR");
        console.error(err);
      }
      records.forEach(function (record) {
        console.log("END INVENTORY:");
        console.log(record.get("inventory"));
      });
    }
  );
};

exports.handler = async ({ body, headers }) => {
  try {
    // check the webhook to make sure it’s valid
    const stripeEvent = stripe.webhooks.constructEvent(
      body,
      headers["stripe-signature"],
      process.env.STRIPE_WEBHOOK_SECRET
    );

    // only do stuff if this is a successful Stripe Checkout purchase
    if (stripeEvent.type === "checkout.session.completed") {
      console.log("CHECKOUT SESSION COMPLETED");
      const eventObject = stripeEvent.data.object;
      const id = eventObject.metadata.airtableId;
      console.log(id);
      // const testId = "recBaKuw8TY27ndSj";
      const record = await table.find(id);
      const prevInventory = record.fields.inventory;
      console.log(prevInventory);
      if (prevInventory > 0) {
        console.log("inventory greater than 0");
        decrementInventory(record.fields.idCalculation, prevInventory - 1);
      }
    }

    return {
      statusCode: 200,
      body: JSON.stringify({ received: true }),
    };
  } catch (err) {
    console.log(`Stripe webhook failed with ${err}`);

    return {
      statusCode: 400,
      body: `Webhook Error: ${err.message}`,
    };
  }
};

CLI LOGS (successful)

Request from ::1: POST /.netlify/functions/decrement-inventory
Response with status 200 in 4 ms.
Request from ::1: POST /.netlify/functions/decrement-inventory
Response with status 200 in 4 ms.
Request from ::1: POST /.netlify/functions/decrement-inventory
Response with status 200 in 4 ms.
Request from ::1: POST /.netlify/functions/decrement-inventory
Response with status 200 in 5 ms.
Request from ::1: POST /.netlify/functions/decrement-inventory
CHECKOUT SESSION COMPLETED
undefined
5
inventory greater than 0
IN DECREMENT FUNCTION
recBaKuw8TY27ndSj 4
Response with status 200 in 505 ms.
END INVENTORY:
4

PRODUCTION LOGS (unsuccessful)

Feb 27, 05:56:38 PM: d9ebc1bf INFO   CHECKOUT SESSION COMPLETED
Feb 27, 05:56:38 PM: d9ebc1bf INFO   recBaKuw8TY27ndSj
Feb 27, 05:56:38 PM: d9ebc1bf INFO   4
Feb 27, 05:56:38 PM: d9ebc1bf INFO   inventory greater than 0
Feb 27, 05:56:38 PM: d9ebc1bf INFO   IN DECREMENT FUNCTION
Feb 27, 05:56:38 PM: d9ebc1bf INFO   recBaKuw8TY27ndSj 3
Feb 27, 05:56:38 PM: d9ebc1bf Duration: 219.65 ms	Memory Usage: 71 MB	Init Duration: 328.72 ms

Disclaimer: I’m still pretty new to http requests and serverless functions so it’s causing me a lot of headache. I thought perhaps the code executes differently in production and is executing after the table.update before it has time to finish, but I’m really not sure as I imagine that would happen in the development environment as well…

Thanks for reading! If you can help or point me in the right direction I would appreciate it. :slight_smile:

Hey @jncdv,

Try changing the code to:

const decrementInventory = (record, newInventory) => {
  console.log("IN DECREMENT FUNCTION");
  console.log(record, newInventory);
  return new Promise((resolve, reject) => {
    table.update([{
      id: record,
      fields: {
        inventory: newInventory,
      },
    }]).then(() => {
      //do some stuff and..
      resolve()
    }).catch(error => {
      reject(error)
    })
  })
}

and see if it works. If it does, I can explain why.

Hey @hrishikesh, thanks for the fast reply!

Unfortunately, I don’t see a difference in the production function. I have changed the code to this:

const decrementInventory = (record, newInventory) => {
  console.log("IN DECREMENT FUNCTION");
  console.log(record, newInventory);
  return new Promise((resolve, reject) => {
    table
      .update([
        {
          id: record,
          fields: {
            inventory: newInventory,
          },
        },
      ])
      .then(() => {
        console.log("resolved");
        resolve();
      })
      .catch((error) => {
        reject(error);
        console.log("rejected");
      });
  });

and as you can see nothing is logged:

Feb 27, 07:22:22 PM: f7df9852 INFO   CHECKOUT SESSION COMPLETED
Feb 27, 07:22:22 PM: f7df9852 INFO   recBaKuw8TY27ndSj
Feb 27, 07:22:22 PM: f7df9852 INFO   4
Feb 27, 07:22:22 PM: f7df9852 INFO   inventory greater than 0
Feb 27, 07:22:22 PM: f7df9852 INFO   IN DECREMENT FUNCTION
Feb 27, 07:22:22 PM: f7df9852 INFO   recBaKuw8TY27ndSj 3
Feb 27, 07:22:22 PM: f7df9852 Duration: 155.74 ms	Memory Usage: 71 MB	Init Duration: 389.90 ms	

Any ideas? Thanks again for your help

EDIT: I added the await keyword before the decrementInventory( ) call in the handler function and it seems to be working! I’m going to double check with a few tests and will report back!

EDIT2: Ok, I’ve tested it a few times in different browsers (I thought this was an issue before) and it looks like it’s always working and logging the resolution of the promise now that I added await

@hrishikesh thank you so much! Do you mind explaining why this was working without awaiting the promise in the CLI/development environment and not in deployment? I appreciate it! :slight_smile:

Hey @jncdv,

Sorry for the delay. Glad to know that worked (and yes, I totally forgot to ask you to use await for your decrementInventory( ) call.

To add the explanation:

A Promise explicitly asks the code to wait to do the next step till the Promise either resolves or is rejected. Now, here’s what’s happening. JavaScript will be executed top-down. When you call the decrementInventory() function, JavaScript makes a call to that function, start executing it and it also continues processing the rest of your code. After the decrementInventory() call, you have your return statement which is sending data back to client. So even though the Airbase stuff started running, AWS terminated the Lambda because you send the response. With the await keyword, you’re asking the Lambda to wait for the Promise to either resolve or reject before executing the return statement.

The alternative way to do this without the await keyword would be to use the following:

decrementInventory().then(() => {
  return {
    statusCode: 200,
    body: JSON.stringify({ received: true })
  }
})