fix(stripe): bail out of checkout.session.completed for non-subscription modes (#9684)

Co-authored-by: Taesu <166604494+bytaesu@users.noreply.github.com>
Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
Nikhil kumar gupta
2026-10-01 18:15:28 +00:00
committed by GitHub
co-authored by Taesu Taesu
parent 4175b34a8f
commit 72a6394e30
3 changed files with 79 additions and 1 deletions
@@ -0,0 +1,5 @@
---
"@better-auth/stripe": patch
---
Stop logging a "Stripe webhook failed" error when a one-time `payment` mode checkout session completes.
+8 -1
View File
@@ -46,7 +46,14 @@ export async function onCheckoutSessionCompleted(
try {
const client = options.stripeClient;
const checkoutSession = event.data.object as Stripe.Checkout.Session;
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
/**
* Only `subscription` mode is currently supported.
* @see https://docs.stripe.com/api/checkout/sessions/object#checkout_session_object-mode
*/
if (
checkoutSession.mode !== "subscription" ||
!options.subscription?.enabled
) {
return;
}
const subscription = await client.subscriptions.retrieve(
+66
View File
@@ -2714,4 +2714,70 @@ describe("stripe webhook", () => {
expect(updatedSub!.endedAt!.getTime()).toBe(now * 1000);
});
});
/**
* @see https://github.com/better-auth/better-auth/issues/4565
*/
describe("webhook: non-subscription checkout session modes", () => {
test.for([
"payment",
"setup",
] as const)("mode: %s does not call subscriptions.retrieve or onSubscriptionComplete", async (mode, {
memory,
stripeOptions,
}) => {
const onSubscriptionComplete = vi.fn();
const subscriptionsRetrieve = vi.fn();
const webhookEvent = {
type: "checkout.session.completed",
data: {
object: {
id: `cs_4565_${mode}`,
mode,
subscription: null,
customer: `cus_4565_${mode}`,
metadata: {},
},
},
};
const stripeForTest = {
...stripeOptions.stripeClient,
subscriptions: {
...stripeOptions.stripeClient.subscriptions,
retrieve: subscriptionsRetrieve,
},
webhooks: {
constructEventAsync: vi.fn().mockResolvedValue(webhookEvent),
},
};
const testOptions = {
...stripeOptions,
stripeClient: stripeForTest as unknown as Stripe,
subscription: {
...stripeOptions.subscription,
onSubscriptionComplete,
},
} satisfies StripeOptions;
const { auth: webhookAuth } = await getTestInstance(
{ database: memory, plugins: [stripe(testOptions)] },
{ disableTestUser: true },
);
const response = await webhookAuth.handler(
new Request("http://localhost:3000/api/auth/stripe/webhook", {
method: "POST",
headers: { "stripe-signature": "test_signature" },
body: JSON.stringify(webhookEvent),
}),
);
expect(response.status).toBe(200);
expect(subscriptionsRetrieve).not.toHaveBeenCalled();
expect(onSubscriptionComplete).not.toHaveBeenCalled();
});
});
});