From a000aa4cb7bcfd109ed17c9531cee0e0a2c3d712 Mon Sep 17 00:00:00 2001 From: cavidelizade Date: Fri, 17 Jul 2026 00:13:31 +0400 Subject: [PATCH] fix(api): don't double-run webhook side effects on a concurrent duplicate Webhook dedup was a check-then-act: ExistsByDeliveryID then Create. Two concurrent deliveries of the same event both pass the exists check; one Create wins and the other fails the unique delivery_id constraint. That error was only logged, so the losing racer fell through to dispatch and ran the side effects again (duplicate bot comments, duplicate or oscillating state changes). Treat a duplicate-key error from Create as "already processed" and return without dispatching. Closes #332 Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/internal/service/github_events.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/api/internal/service/github_events.go b/apps/api/internal/service/github_events.go index 334bd708..1316d078 100644 --- a/apps/api/internal/service/github_events.go +++ b/apps/api/internal/service/github_events.go @@ -12,6 +12,7 @@ import ( "github.com/Devlaner/devlane/api/internal/model" "github.com/Devlaner/devlane/api/internal/store" "github.com/google/uuid" + "gorm.io/gorm" ) // GithubEventService processes inbound webhook events: it owns the side-effects @@ -101,6 +102,13 @@ func (s *GithubEventService) HandleWebhook(ctx context.Context, event, deliveryI Status: "received", } if err := s.events.Create(ctx, logRow); err != nil { + // A concurrent duplicate delivery loses the race on the unique + // delivery_id constraint. The winning delivery is already running the + // side effects, so stop here instead of dispatching them again. + if errors.Is(err, gorm.ErrDuplicatedKey) { + s.logger().Debug("github webhook delivery already processed (concurrent)", "delivery_id", deliveryID, "event", event) + return nil + } s.logger().Error("failed to record github webhook event", "delivery_id", deliveryID, "error", err) }