Bug Description
The Stripe webhook handler at apps/web/app/(ee)/api/stripe/webhook/route.ts returns HTTP 400 on business logic errors (e.g., email send failure). Stripe treats 4xx as "retry me," which re-fires events like checkout.session.completed with no idempotency guard — potentially causing double plan upgrades or duplicate processing.
Affected file: (ee)/api/stripe/webhook/route.ts (around line 83)
Expected Behavior
Stripe webhook handlers should return HTTP 200 for all successfully received events, regardless of whether downstream business logic succeeds. Business logic failures should be handled internally (retry queue, dead-letter, logging) — not surfaced as HTTP status codes that trigger Stripe's retry machinery.
Suggested Fix
- Return 200 for all received events:
try {
await handleEvent(event);
} catch (error) {
// Log error, enqueue for retry internally — but don't tell Stripe to retry
console.error("Webhook processing failed:", error);
await deadLetterQueue.add(event);
}
return NextResponse.json({ received: true }, { status: 200 });
- Add event-ID deduplication (prevents double-processing on legitimate Stripe retries):
const eventId = event.id;
const alreadyProcessed = await redis.set(`stripe:event:${eventId}`, "1", { nx: true, ex: 86400 });
if (!alreadyProcessed) {
return NextResponse.json({ received: true, deduplicated: true }, { status: 200 });
}
Impact
Without this fix, any transient downstream failure (email service timeout, database blip) causes Stripe to re-deliver the event up to its retry limit, and each delivery re-executes the full handler including subscription updates and workspace plan changes.
Found via ADQB architecture audit
Bug Description
The Stripe webhook handler at
apps/web/app/(ee)/api/stripe/webhook/route.tsreturns HTTP 400 on business logic errors (e.g., email send failure). Stripe treats 4xx as "retry me," which re-fires events likecheckout.session.completedwith no idempotency guard — potentially causing double plan upgrades or duplicate processing.Affected file:
(ee)/api/stripe/webhook/route.ts(around line 83)Expected Behavior
Stripe webhook handlers should return HTTP 200 for all successfully received events, regardless of whether downstream business logic succeeds. Business logic failures should be handled internally (retry queue, dead-letter, logging) — not surfaced as HTTP status codes that trigger Stripe's retry machinery.
Suggested Fix
Impact
Without this fix, any transient downstream failure (email service timeout, database blip) causes Stripe to re-deliver the event up to its retry limit, and each delivery re-executes the full handler including subscription updates and workspace plan changes.
Found via ADQB architecture audit