Skip to content

Instantly share code, notes, and snippets.

@JoshSalway
Created March 18, 2026 07:15
Show Gist options
  • Select an option

  • Save JoshSalway/410ee932634f56a1a4134bb116b1ea95 to your computer and use it in GitHub Desktop.

Select an option

Save JoshSalway/410ee932634f56a1a4134bb116b1ea95 to your computer and use it in GitHub Desktop.
Fix webhook race condition causing duplicate subscriptions (laravel/cashier-stripe #1759)
diff --git a/src/Http/Controllers/WebhookController.php b/src/Http/Controllers/WebhookController.php
index e821f6c..43d0d0f 100644
--- a/src/Http/Controllers/WebhookController.php
+++ b/src/Http/Controllers/WebhookController.php
@@ -2,6 +2,7 @@
namespace Laravel\Cashier\Http\Controllers;
+use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\Request;
use Illuminate\Notifications\Notifiable;
use Illuminate\Routing\Controller;
@@ -80,25 +81,31 @@ protected function handleCustomerSubscriptionCreated(array $payload)
$firstItem = $data['items']['data'][0];
$isSinglePrice = count($data['items']['data']) === 1;
- $subscription = $user->subscriptions()->updateOrCreate([
- 'stripe_id' => $data['id'],
- ], [
- 'type' => $data['metadata']['type'] ?? $data['metadata']['name'] ?? $this->newSubscriptionType($payload),
- 'stripe_status' => $data['status'],
- 'stripe_price' => $isSinglePrice ? $firstItem['price']['id'] : null,
- 'quantity' => $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null,
- 'trial_ends_at' => $trialEndsAt,
- 'ends_at' => null,
- ]);
-
- foreach ($data['items']['data'] as $item) {
- $subscription->items()->updateOrCreate([
- 'stripe_id' => $item['id'],
+ try {
+ $subscription = $user->subscriptions()->updateOrCreate([
+ 'stripe_id' => $data['id'],
], [
- 'stripe_product' => $item['price']['product'],
- 'stripe_price' => $item['price']['id'],
- 'quantity' => $item['quantity'] ?? null,
+ 'type' => $data['metadata']['type'] ?? $data['metadata']['name'] ?? $this->newSubscriptionType($payload),
+ 'stripe_status' => $data['status'],
+ 'stripe_price' => $isSinglePrice ? $firstItem['price']['id'] : null,
+ 'quantity' => $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null,
+ 'trial_ends_at' => $trialEndsAt,
+ 'ends_at' => null,
]);
+ } catch (UniqueConstraintViolationException $e) {
+ $subscription = $user->subscriptions()->where('stripe_id', $data['id'])->first();
+ }
+
+ if ($subscription) {
+ foreach ($data['items']['data'] as $item) {
+ $subscription->items()->updateOrCreate([
+ 'stripe_id' => $item['id'],
+ ], [
+ 'stripe_product' => $item['price']['product'],
+ 'stripe_price' => $item['price']['id'],
+ 'quantity' => $item['quantity'] ?? null,
+ ]);
+ }
}
}
@@ -182,7 +189,41 @@ protected function handleCustomerSubscriptionUpdated(array $payload)
$subscription->stripe_status = $data['status'];
}
- $subscription->save();
+ try {
+ $subscription->save();
+ } catch (UniqueConstraintViolationException $e) {
+ // A race condition with the customer.subscription.created webhook
+ // may cause a duplicate insert attempt. Fetch the existing record
+ // and update it instead so no data is lost.
+ $subscription = $user->subscriptions()->where('stripe_id', $data['id'])->first();
+
+ if (! $subscription) {
+ throw $e;
+ }
+
+ $subscription->fill([
+ 'type' => $subscription->type ?? $data['metadata']['type'] ?? $data['metadata']['name'] ?? $this->newSubscriptionType($payload),
+ 'stripe_price' => $isSinglePrice ? $firstItem['price']['id'] : null,
+ 'quantity' => $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null,
+ 'stripe_status' => $data['status'] ?? $subscription->stripe_status,
+ ]);
+
+ if (isset($data['trial_end'])) {
+ $subscription->trial_ends_at = Carbon::createFromTimestamp($data['trial_end']);
+ }
+
+ if ($data['cancel_at_period_end'] ?? false) {
+ $subscription->ends_at = $subscription->onTrial()
+ ? $subscription->trial_ends_at
+ : $subscription->currentPeriodEnd();
+ } elseif (isset($data['cancel_at']) || isset($data['canceled_at'])) {
+ $subscription->ends_at = Carbon::createFromTimestamp($data['cancel_at'] ?? $data['canceled_at']);
+ } else {
+ $subscription->ends_at = null;
+ }
+
+ $subscription->save();
+ }
// Update subscription items...
if (isset($data['items'])) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment