#What are the best practices for webhooks in actions?

53 messages · Page 1 of 1 (latest)

keen spire
#

Can I encapsulate the Stripe webhook into my convex Action when I'm using the Stripe integration project? I tried using httpAction before, but encountered verification errors that prevented it from working. Here is the API code in my Next.js application:

#
export const config = { api: { bodyParser: false } }

const buffer = async (req: NextApiRequest): Promise<Buffer> => {
  const chunks: Buffer[] = []

  for await (const chunk of req) {
    chunks.push(Buffer.from(chunk))
  }

  return Buffer.concat(chunks)
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<{ invoiceId?: string; error?: string }>,
) {
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST')
    res.status(405).json({ error: 'Method Not Allowed' })
    return
  }

  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string
  const sig = req.headers['stripe-signature'] as string

  try {
    const event = stripe.webhooks.constructEvent(
      await buffer(req),
      sig,
      webhookSecret,
    )
    if (event.type === 'invoice.payment_succeeded') {
      const invId = (event.data.object as { id: string }).id
      fetch(`${process.env.CONVEX_SITE}/invoice?invId=${invId}`)
      res.status(200).json({ invoiceId: invId })
    }
    res.status(200).end()
  } catch (err) {
    res.status(400).json({ error: 'Webhook Error' })
  }
}
#

Here is my http api code:```js
http.route({
path: '/invoice',
method: 'GET',
handler: httpAction(async ({ runMutation }, request) => {
const invId = new URL(request.url).searchParams.get('invId')
if (invId) {
await runMutation('xxx:create', {
...
})

  return new Response(null, {
    status: 200,
  })
} else {
  return new Response('invId is required', {
    status: 400,
  })
}

}),
})

#

My development environment consists of Next.js v12 + Node v20 + Convex v0.16.

#

Invoking the API request returns a 500 status code, along with the following error message:```json
"error_message": "Uncaught Error: Uncaught Error: Unauthenticated call to mutation\n at handler (../convex/usage.ts:54:4)\n at async invokeMutation (../../node_modules/convex/src/server/impl/registration_impl.ts:52:2)\n"

#

Additionally, I have implemented relevant validations within the create function.

#
const identity = await auth.getUserIdentity()
if (!identity) {
  throw new Error('Unauthenticated call to mutation')
}
#

Is there a way for me to migrate this webhook API from Next.js to a convex Action, thereby avoiding the use of httpAction?

nocturne sun
#

Hey @keen spire, ingesting stripe webhooks with httpAction is totally possible! In this case, it looks like your xxx:create function is expecting auth to be setup, but since this request is coming from stripe, there is no user to authenticate. So, I'd recommend using an internalMutation here instead so it can only be run by other convex functions

#

You can certainly migrate this next function to Convex! I've done this in one of my apps before, I can send you an example in a bit, once I make it into the office

keen spire
#

Thank you very much @nocturne sun , and I noticed that the official documentation has an integration usage of ***ConvexHttpClient ***with Next.js. I'm not sure if this would also work for my webhook or if the authorization issue would still arise.

nocturne sun
#

Oh hm I see! Would you like to keep using nextjs for receiving the stripe webhook? You can actually setup convex to replace that nextjs function altogether!

#

Stripe can send the webhook events directly to your convex httpAction, and that action could do the stripe webhook secret validation, and perform all your mutations

#

(Going afk to drive in to work, but will be back in about an hour)

lapis trench
#

@keen spire essentially, what arnold is suggesting is the http action can be the webhook and authenticate the call from stripe. and then you can call subsequent "internal" mutations without worrying about authentication information tunneling into that layer because convex ensures internal mutations can only be called by your own code, by authorized contexts

keen spire
#

Cool! I get it. I can use the convex http action as the endpoint for the webhook, without the need for an additional Next/api. This way, I can avoid the hassle of passing authentication around.

lapis trench
#

you got it

keen spire
#

This is the code after I migrated to http action:```js
http.route({
path: '/invoice',
method: 'POST',
handler: httpAction(async ({ runMutation }, request) => {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', {
status: 405,
headers: { Allow: 'POST' },
})
}

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string
const sig = request.headers.get('stripe-signature') as string

try {
  const event = stripe.webhooks.constructEvent(
    await request.text(),
    sig,
    webhookSecret,
  )
  if (event.type === 'invoice.payment_succeeded') {
    const invId = (event.data.object as { id: string }).id
    await runMutation('xxx:create', {
      ...
    })
  }
  return new Response(null, {
    status: 200,
  })
} catch (err) {
  return new Response('Webhook Error', {
    status: 400,
  })
}

}),
})

#

but errors occurred: ```json
✘ [ERROR] Could not resolve "stream"

node_modules/next/dist/compiled/@vercel/og/index.node.js:17949:25:
  17949 │ ...rt { Readable } from "stream";
        ╵                         ~~~~~~~~

The package "stream" wasn't found on the file
system but is built into node. Are you trying
to bundle for node? You can use "platform:
'node'" to do that, which will remove this
error.

✘ [ERROR] Could not resolve "fs"

node_modules/next/dist/compiled/@vercel/og/index.node.js:17950:16:
  17950 │ import fs2 from "fs";
        ╵                 ~~~~

The package "fs" wasn't found on the file
system but is built into node. Are you trying
to bundle for node? You can use "platform:
'node'" to do that, which will remove this
error.

✘ [ERROR] No loader is configured for ".wasm" files: node_modules/next/dist/compiled/@vercel/og/yoga.wasm?module

node_modules/next/dist/compiled/@vercel/og/index.edge.js:17950:22:
  17950 │ ...asm from "./yoga.wasm?module";
        ╵             ~~~~~~~~~~~~~~~~~~~~

✘ [ERROR] No loader is configured for ".wasm" files: node_modules/next/dist/compiled/@vercel/og/resvg.wasm?module

node_modules/next/dist/compiled/@vercel/og/index.edge.js:17949:23:
  17949 │ ...sm from "./resvg.wasm?module";
        ╵            ~~~~~~~~~~~~~~~~~~~~~
#

Can you provide me with some more guidance? Thank you very much!

lapis trench
#

looks like your action is bundling vercel open graph image generation stuff. I'm guessing you probably don't need that in your webhook. maybe @restive axle has an idea here on how to manage this?

keen spire
#

Ah! I found the cause of this bug. It was due to referencing the stripe object defined in Next.js from within Convex, resulting in an incompatible runtime environment error. After redefining a new stripe object, the bug was resolved.

#

Thank you to the developers of Convex @lapis trench @nocturne sun for their assistance. If there are any issues regarding webhooks in the future, I will continue to follow up here. 🙂

lapis trench
#

sounds great @keen spire ! glad to hear you're unblocked and moving forward on your project.

keen spire
#

Can someone give me a sample code about stripe webhook? Because it takes the original object of the request (buffer or string) as a parameter.

#

Something err like this:

#

400 Bad Request: InvalidModules: Loading the pushed modules encountered the following error: Failed to analyze http.js: Uncaught ReferenceError: Event is not defined at <anonymous> (../node_modules/stripe/esm/StripeEmitter.js:6:16)

keen spire
#
{
  "error_message": "Uncaught Error: Converting circular structure to JSON\n    --> starting at object with constructor 'Stripe2'\n    |     property 'account' -> object with constructor 'Constructor'\n    --- property '_stripe' closes the circle\n  at stringifyValueForError (../node_modules/convex/src/values/value.ts:375:8)\n  at convexToJsonInternal (../node_modules/convex/src/values/value.ts:484:10)\n  at convexToJsonInternal (../node_modules/convex/src/values/value.ts:501:15)\n  at convexToJson (../node_modules/convex/src/values/value.ts:549:0)\n  at invokeAction (../node_modules/convex/src/server/impl/registration_impl.ts:250:0)\n"
}
nocturne sun
#

Hey, this does look like an odd error! Would you mind sharing the code you have so far? Feel free to DM it to me, but be sure to remove any secrets from the code

keen spire
#

@nocturne sun Of course I can! Here is my code in the project:

#

action.ts```js

export const createStripe = internalAction({
handler: async () => {
return new Stripe(process.env.STRIPE_SECRET_KEY as string, {
apiVersion: '2022-11-15',
})
},
})

#

http.ts```js
const http = httpRouter()

http.route({
path: '/invoice',
method: 'POST',
handler: httpAction(async ({ runMutation, runAction }, request) => {
const stripe = await runAction('action:createStripe')
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string
const sig = request.headers.get('stripe-signature') as string

try {
  const event = stripe.webhooks.constructEvent(
    await request.text(),
    sig,
    webhookSecret,
  )
  if (event.type === 'invoice.payment_succeeded') {
    const invId = (event.data.object as { id: string }).id
    await runMutation('xxx:create', { invoice: invId })
  }
  return new Response(null, {
    status: 200,
  })
} catch (err) {
  return new Response('Webhook Error', {
    status: 400,
  })
}

}),
})

export default http

#

At first I thought that the reason for the error was that I did not pass the **original object of the request **as a parameter to the stripe webhook (http defaults to the Request type of the fetch api? I am not sure if this has any effect) but after debugging, I found that the same error still occurs , so the possibility of this error should be ruled out?

keen spire
#

By the way, I am using version 0.16 of convex

nocturne sun
#

Thanks! OK I see what's going on now. the createStripe action returns the stripe class, but you can't exchange classes between convex actions, only data. My suggestion would be to use your HTTP action only for processing the request an response, and pushing all the logic down into a single internalAction

#

The code would roughly look like this:

#
// http.ts
const http = httpRouter();

http.route({
  path: "/invoice",
  method: "POST",
  handler: httpAction(async ({ runAction }, request) => {
    const signature: string = request.headers.get("stripe-signature") as string;

    const result = await runAction("actions/stripe:webhook", {
      sig: signature,
      payload: await request.text(),
    });

    if (result.success) {
      return new Response(null, {
        status: 200,
      });
    } else {
      return new Response("Webhook Error", {
        status: 400,
      });
    }
  }),
});
#
// actions/stripe.ts
export const webhook = internalAction({
  handler: async ({ runMutation }, { sig: string, payload: string }) => {
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
      apiVersion: "2022-11-15",
    });
    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string;
    try {
      const event = stripe.webhooks.constructEvent(payload, sig, webhookSecret);
      if (event.type === "invoice.payment_succeeded") {
        const invId = (event.data.object as { id: string }).id;
        await runMutation("xxx:create", { invoice: invId });
      }
      return { success: true };
    } catch (err) {
      console.error(err);
      return { success: false, error: err.message };
    }
  },
});```
keen spire
#

Awesome! Now all the issues with webhooks are finally resolved perfectly. If in the future we can "use node" in HTTP, then can we migrate the core logic to it without the need to define it in the action (although that way is also concise)?In any case, I'm extremely grateful for @nocturne sun assistance!

nocturne sun
#

Awesome glad it's working! Time to get paid with Stripe 😄

dense vessel
main saddle
#

Thanks for the article, I'm most of the way through but stuck on the webhooks.

#

Is there anything else that needs to be done to allow the webhooks to call?

#

It's a 404 from the stipe end

#

The endpoint shows up in the convex dashboard but it's never been hit

#

I'm probably missing something simple

#

Okay, I figured it out!

#

It wasn't clear on the article that it was meant to point to
".convex.site"

I had it pointing to:
"convex.cloud/stripe"

low mango
#

Thanks for following up!