Skip to content

Commit 4179bf3

Browse files
fix(plugin-stripe): supports webhooks within serverless environments (#10890)
Fixes #10639. 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 opens 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 For Vercel specifically, the fix here is to conditionally import `waitUntil` from the `@vercel/functions` package. This will add without affecting other serverless environments. If you're on Vercel, you can install the `@vercel/functions` package. When detected, this plugin wraps all webhook handlers with the `waitUntil` function provided by this module. 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 --------- Co-authored-by: Paul Popus <paul@payloadcms.com>
1 parent b44135e commit 4179bf3

File tree

3 files changed

+73
-22
lines changed

3 files changed

+73
-22
lines changed

docs/plugins/stripe.mdx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,29 @@ export default config
177177

178178
For a full list of available webhooks, see [here](https://stripe.com/docs/cli/trigger#trigger-event).
179179

180-
## Node
180+
### Serverless Environments
181+
182+
When using the Stripe Plugin within a serverless environment, the process that handles running webhooks will immediately close before these webhooks finish executing.
183+
184+
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.
185+
186+
From the Stripe docs:
187+
188+
> 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.
189+
190+
Reference: https://docs.stripe.com/webhooks#acknowledge-events-immediately
191+
192+
#### Vercel
193+
194+
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 allows the serverless function instance to stay alive and complete processing after the response has been sent.
195+
196+
From the Vercel docs:
197+
198+
> 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.
199+
200+
Reference: https://vercel.com/docs/functions/functions-api-reference#waituntil
201+
202+
## Node.js
181203

182204
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:
183205

packages/plugin-stripe/src/routes/webhooks.ts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Config as PayloadConfig, PayloadRequest } from 'payload'
22

3+
import { dynamicImport } from 'payload'
34
import Stripe from 'stripe'
45

56
import type { StripePluginConfig } from '../types.js'
@@ -41,31 +42,19 @@ export const stripeWebhooks = async (args: {
4142
}
4243

4344
if (event) {
44-
void handleWebhooks({
45-
config,
46-
event,
47-
payload: req.payload,
48-
pluginConfig,
49-
req,
50-
stripe,
51-
})
52-
53-
// Fire external webhook handlers if they exist
54-
if (typeof webhooks === 'function') {
55-
void webhooks({
45+
const fireWebhooks = async () => {
46+
await handleWebhooks({
5647
config,
5748
event,
5849
payload: req.payload,
5950
pluginConfig,
6051
req,
6152
stripe,
6253
})
63-
}
6454

65-
if (typeof webhooks === 'object') {
66-
const webhookEventHandler = webhooks[event.type]
67-
if (typeof webhookEventHandler === 'function') {
68-
void webhookEventHandler({
55+
// Fire external webhook handlers if they exist
56+
if (typeof webhooks === 'function') {
57+
await webhooks({
6958
config,
7059
event,
7160
payload: req.payload,
@@ -74,7 +63,47 @@ export const stripeWebhooks = async (args: {
7463
stripe,
7564
})
7665
}
66+
67+
if (typeof webhooks === 'object') {
68+
const webhookEventHandler = webhooks[event.type]
69+
if (typeof webhookEventHandler === 'function') {
70+
await webhookEventHandler({
71+
config,
72+
event,
73+
payload: req.payload,
74+
pluginConfig,
75+
req,
76+
stripe,
77+
})
78+
}
79+
}
7780
}
81+
82+
/**
83+
* 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.
84+
* This is because webhooks can be potentially slow if performing database queries or other slow API requests.
85+
* 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.
86+
* When a webhook fails, Stripe will retry it, causing duplicate events and potential data inconsistencies.
87+
*
88+
* To do this in Vercel environments, conditionally import the `waitUntil` function from `@vercel/functions`.
89+
* If it exists, use it to wrap the `fireWebhooks` function to ensure it completes after the response is sent.
90+
* Otherwise, run the `fireWebhooks` function directly and void the promise to prevent the response from waiting.
91+
* {@link https://docs.stripe.com/webhooks#acknowledge-events-immediately}
92+
*/
93+
void (async () => {
94+
let waitUntil: (promise: Promise<void>) => Promise<void> | void = (promise) => promise
95+
96+
try {
97+
const { waitUntil: importedWaitUntil } = await dynamicImport<{
98+
waitUntil: (promise: Promise<void>) => void
99+
}>('@vercel/functions')
100+
waitUntil = importedWaitUntil
101+
} catch (_err) {
102+
// silently fail - @vercel/functions is not installed
103+
}
104+
105+
void waitUntil(fireWebhooks())
106+
})()
78107
}
79108
}
80109
}

packages/plugin-stripe/src/webhooks/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { StripeWebhookHandler } from '../types.js'
33
import { handleCreatedOrUpdated } from './handleCreatedOrUpdated.js'
44
import { handleDeleted } from './handleDeleted.js'
55

6-
export const handleWebhooks: StripeWebhookHandler = (args) => {
6+
export const handleWebhooks: StripeWebhookHandler = async (args) => {
77
const { event, payload, pluginConfig } = args
88

99
if (pluginConfig?.logs) {
@@ -22,7 +22,7 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
2222
if (syncConfig) {
2323
switch (method) {
2424
case 'created': {
25-
void handleCreatedOrUpdated({
25+
await handleCreatedOrUpdated({
2626
...args,
2727
pluginConfig,
2828
resourceType,
@@ -31,7 +31,7 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
3131
break
3232
}
3333
case 'deleted': {
34-
void handleDeleted({
34+
await handleDeleted({
3535
...args,
3636
pluginConfig,
3737
resourceType,
@@ -40,7 +40,7 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
4040
break
4141
}
4242
case 'updated': {
43-
void handleCreatedOrUpdated({
43+
await handleCreatedOrUpdated({
4444
...args,
4545
pluginConfig,
4646
resourceType,

0 commit comments

Comments
 (0)