Skip to content

fix(claw): prevent double-tap race condition in switchPlan#1377

Open
jeanduplessis wants to merge 2 commits intomainfrom
kiloclaw-billing-plan-switch
Open

fix(claw): prevent double-tap race condition in switchPlan#1377
jeanduplessis wants to merge 2 commits intomainfrom
kiloclaw-billing-plan-switch

Conversation

@jeanduplessis
Copy link
Contributor

Summary

  • Fix a race condition in kiloclaw.switchPlan where concurrent requests (e.g. mobile double-tap) could both pass the DB guard and create duplicate Stripe subscription schedules, causing a 500 error on the second request.
  • Stripe-side pre-check: Before creating a schedule, retrieve the subscription from Stripe and release any orphaned schedule (attached on Stripe but not tracked in DB) from prior failed/racing requests.
  • Optimistic concurrency: The DB write uses WHERE stripe_schedule_id IS NULL so only the first concurrent request succeeds; losers release their schedule and return a CONFLICT error.
  • try/catch with cleanup: Schedule creation is wrapped in try/catch — if any step fails after Stripe schedule creation, the orphaned schedule is released before rethrowing.
  • cancelPlanSwitch error handling: Tolerates StripeInvalidRequestError when the schedule is already released, so users aren't stuck with stale DB state.
  • Client-side: Disable "Switch to..." and "Cancel Switch" buttons during pending mutations to prevent double-tap at the UI level.

Verification

  • pnpm typecheck — passed
  • pnpm jest src/routers/kiloclaw-billing-router.test.ts — 39/39 passed (12 new tests for switchPlan and cancelPlanSwitch covering happy path, validation, orphan cleanup, optimistic concurrency race, and Stripe error tolerance)

Visual Changes

N/A

Reviewer Notes

  • The optimistic concurrency test simulates a true in-flight race by injecting a concurrent DB write inside the subscriptionSchedules.update mock, so the handler's initial DB read sees no schedule but the conditional UPDATE ... WHERE stripe_schedule_id IS NULL returns 0 rows.
  • The existing DB-level guard (if (sub.stripe_schedule_id)) is preserved for legitimate pending switches — only orphaned Stripe schedules (not tracked in DB) are auto-released.
  • Affects Sentry issues KILOCODE-WEB-1G1M (server) and KILOCODE-WEB-1FZH (client), impacting 4 users with 8 error events since 2026-03-19.

…phan cleanup

Implement switchPlan and cancelPlanSwitch tRPC endpoints with:
- Optimistic concurrency guard on DB write to prevent double-schedule races
- Orphaned Stripe schedule detection and cleanup before creating new schedules
- Best-effort rollback if Stripe schedule update fails after creation
- Idempotent cancel that tolerates already-released schedules
- Disabled button states during pending mutations in SubscriptionCard
- 11 new tests covering happy path, edge cases, and race conditions
@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Mar 22, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
src/routers/kiloclaw-router.ts 1392 Swallowing every orphan-schedule release error can continue after a transient Stripe failure and still try to create a new schedule against a subscription that may already have one attached.
src/routers/kiloclaw-router.ts 1503 The already-released matcher does not include canceled schedules, so cancelPlanSwitch can still leave stale DB state behind when Stripe reports that status.
Other Observations (not in diff)

None.

Files Reviewed (3 files)
  • src/routers/kiloclaw-router.ts - 2 issues
  • src/routers/kiloclaw-billing-router.test.ts - 0 issues
  • src/app/(app)/claw/components/billing/SubscriptionCard.tsx - 0 issues

Reviewed by gpt-5.4-20260305 · 502,651 tokens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant