fix: prevent double-credit race condition with atomic conditional UPDATE#43
Open
memosr wants to merge 1 commit into
Open
fix: prevent double-credit race condition with atomic conditional UPDATE#43memosr wants to merge 1 commit into
memosr wants to merge 1 commit into
Conversation
|
Reviewed the fix independently. The atomic conditional UPDATE pattern is correct — replacing check-then-write with a single UPDATE ... WHERE status='pending' ... RETURNING eliminates the race window at the database level, which is the only reliable place to enforce this guarantee. Verified the two code paths described in the problem statement exist at the locations mentioned. The fix correctly handles both the PATCH handler and the webhook path separately, which is important because their state transitions differ. One note: the early-return guard at line 209 in the PATCH handler is now correctly documented as an optimization, not a race guard. This distinction matters for future readers. LGTM from a correctness standpoint. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two code paths can credit the same transaction, creating a race condition that allows double-crediting:
app/api/transactions/[id]/route.ts:235— guards withstatus !== 'pending'at line 209app/api/circle/webhook/route.ts:152— guards withwasAlreadyProcessedat line 148Both paths follow the same vulnerable pattern: read status → check guard → write update + increment credits. The check-then-write is not atomic.
The race:
Time PATCH handler Circle webhook
────────────────────────────────────────────────────────────
t=0 SELECT status → 'pending'
t=1 SELECT status → 'pending'
t=2 Check passes ✓ Check passes ✓
t=3 increment_credits()
t=4 increment_credits() ← DUPLICATE
t=5 UPDATE status = 'complete'
t=6 UPDATE status = 'complete'
Fix
Replace the "check then update" pattern with an atomic conditional UPDATE that only succeeds if the status is still
'pending':This pattern ensures that even if both paths read
'pending'simultaneously, only one UPDATE actually flips the status — the other getsnullback and skips the credit increment.Changes
app/api/transactions/[id]/route.ts(PATCH handler)increment_creditscall after the atomic UPDATE.eq("status", "pending")and uses.maybeSingle()app/api/circle/webhook/route.ts(updateTransactionStatus)pending → confirmed/complete): atomic UPDATE withWHERE status='pending', credit only if row returnedconfirmed → complete,any → failed): plain UPDATE — no double-spend riskImpact
The existing
.single()calls in unrelated paths (GET handler ownership check, admin transaction updates) are untouched since they're not part of the credit flow.