Compare commits

...

3 Commits

Author SHA1 Message Date
Jacob Fletcher
c6481e109b conditionally imports waitUntil from @vercel/functions 2025-01-31 13:37:33 -05:00
Jacob Fletcher
d7520986df Revert "fix(plugin-stripe): supports webhooks within serverless environments"
This reverts commit bd85783d89.
2025-01-31 12:50:21 -05:00
Jacob Fletcher
bd85783d89 fix(plugin-stripe): supports webhooks within serverless environments 2025-01-30 11:41:05 -05:00
3 changed files with 71 additions and 22 deletions

View File

@@ -174,7 +174,29 @@ export default config
For a full list of available webhooks, see [here](https://stripe.com/docs/cli/trigger#trigger-event).
## Node
### Serverless Environments
When using the Stripe Plugin within a serverless environment, the process that handles running webhooks will immediately close before these webhooks finish executing.
The reason for this is that the webhook handler that this plugin processes webhooks asynchronously. This is because Stripe places a timeout on these requests, and if the request takes longer than 10-20 seconds to respond with a 2xx status code, it assumes that the event has failed and will retry at a later date. Stripe expects an immediate response from your application. If your webhooks interact with the database or perform other slow API requests, for example, this can lead to multiple, duplicative events, and potentially data inconsistencies.
From the Stripe docs:
> Your endpoint must quickly return a successful status code (2xx) prior to any complex logic that could cause a timeout. For example, you must return a 200 response before updating a customer's invoice as paid in your accounting system.
Reference: https://docs.stripe.com/webhooks#acknowledge-events-immediately
#### Vercel
If you're on Vercel, you can install the `@vercel/functions` package. When detected, we wrap your custom webhook handler in `waitUntil` function that this module provides. This will process the function in another thread that stays open even after the webhook handler has sent its response.
From the Vercel docs:
> The waitUntil() method enqueues an asynchronous task to be performed during the lifecycle of the request. You can use it for anything that can be done after the response is sent, such as logging, sending analytics, or updating a cache, without blocking the response from being sent.
Reference: https://vercel.com/docs/functions/functions-api-reference#waituntil
## Node.js
On the server you should interface with Stripe directly using the [stripe](https://www.npmjs.com/package/stripe) npm module. That might look something like this:

View File

@@ -41,18 +41,8 @@ export const stripeWebhooks = async (args: {
}
if (event) {
handleWebhooks({
config,
event,
payload: req.payload,
pluginConfig,
req,
stripe,
})
// Fire external webhook handlers if they exist
if (typeof webhooks === 'function') {
webhooks({
const fireWebhooks = async () => {
await handleWebhooks({
config,
event,
payload: req.payload,
@@ -60,12 +50,10 @@ export const stripeWebhooks = async (args: {
req,
stripe,
})
}
if (typeof webhooks === 'object') {
const webhookEventHandler = webhooks[event.type]
if (typeof webhookEventHandler === 'function') {
webhookEventHandler({
// Fire external webhook handlers if they exist
if (typeof webhooks === 'function') {
await webhooks({
config,
event,
payload: req.payload,
@@ -74,7 +62,46 @@ export const stripeWebhooks = async (args: {
stripe,
})
}
if (typeof webhooks === 'object') {
const webhookEventHandler = webhooks[event.type]
if (typeof webhookEventHandler === 'function') {
await webhookEventHandler({
config,
event,
payload: req.payload,
pluginConfig,
req,
stripe,
})
}
}
}
/**
* Run webhook handlers asynchronously. This allows the request to immediately return a 2xx status code to Stripe without waiting for the webhook handlers to complete.
* This is because webhooks can be potentially slow if performing database queries or other synchronous API requests.
* This is important because Stripe will retry the webhook if it doesn't receive a 2xx status code within the 10-20 second timeout window.
* When a webhook fails, Stripe will retry it, causing duplicate events and potential data inconsistencies.
*
* To do this in Vercel environments, conditionally import the `waitUntil` function from `@vercel/functions`.
* If it exists, use it to wrap the `fireWebhooks` function to ensure it completes before the response is sent.
* Otherwise, run the `fireWebhooks` function directly and void the promise to prevent the response from waiting.
* {@link https://docs.stripe.com/webhooks#acknowledge-events-immediately}
*/
void (async () => {
let waitUntil = (promise: Promise<void>) => promise
try {
// @ts-expect-error - Ignore TS error for missing module
const { waitUntil: importedWaitUntil } = await import('@vercel/functions')
waitUntil = importedWaitUntil
} catch (_err) {
// silently fail
}
void waitUntil(fireWebhooks())
})()
}
}
}

View File

@@ -3,7 +3,7 @@ import type { StripeWebhookHandler } from '../types.js'
import { handleCreatedOrUpdated } from './handleCreatedOrUpdated.js'
import { handleDeleted } from './handleDeleted.js'
export const handleWebhooks: StripeWebhookHandler = (args) => {
export const handleWebhooks: StripeWebhookHandler = async (args) => {
const { event, payload, pluginConfig } = args
if (pluginConfig?.logs) {
@@ -22,7 +22,7 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
if (syncConfig) {
switch (method) {
case 'created': {
void handleCreatedOrUpdated({
await handleCreatedOrUpdated({
...args,
pluginConfig,
resourceType,
@@ -31,7 +31,7 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
break
}
case 'deleted': {
void handleDeleted({
await handleDeleted({
...args,
pluginConfig,
resourceType,
@@ -40,7 +40,7 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
break
}
case 'updated': {
void handleCreatedOrUpdated({
await handleCreatedOrUpdated({
...args,
pluginConfig,
resourceType,