Skip to content

Stripe webhook handler returns 4xx on business logic errors, causing unguarded retries #3752

@TsekaLuk

Description

@TsekaLuk

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

  1. 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 });
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions