From 3a37ea32f776d57c79fac98f04fe240e72203c73 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 16 Mar 2026 10:24:16 -0400 Subject: [PATCH] Webpush: Fix FK issue with Postgres --- docs/releases.md | 1 + webpush/store.go | 6 ++++-- webpush/store_postgres.go | 1 + webpush/store_sqlite.go | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 6c3aa94a..eb702f4d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1791,4 +1791,5 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** +* Fix race condition in web push subscription causing FK constraint violation when concurrent requests hit the same endpoint * Route authorization query to read-only database replica to reduce primary database load diff --git a/webpush/store.go b/webpush/store.go index 02b7552e..f6422a79 100644 --- a/webpush/store.go +++ b/webpush/store.go @@ -26,6 +26,7 @@ var ( type Store struct { db *db.DB queries queries + } // queries holds the database-specific SQL queries. @@ -63,9 +64,10 @@ func (s *Store) UpsertSubscription(endpoint string, auth, p256dh, userID string, } else if err != nil { return err } - // Insert or update subscription + // Insert or update subscription, and read back the actual ID (which may differ from + // the generated one if another request for the same endpoint raced us and inserted first) updatedAt, warnedAt := time.Now().Unix(), 0 - if _, err := tx.Exec(s.queries.upsertSubscription, subscriptionID, endpoint, auth, p256dh, userID, subscriberIP.String(), updatedAt, warnedAt); err != nil { + if err := tx.QueryRow(s.queries.upsertSubscription, subscriptionID, endpoint, auth, p256dh, userID, subscriberIP.String(), updatedAt, warnedAt).Scan(&subscriptionID); err != nil { return err } // Replace all subscription topics diff --git a/webpush/store_postgres.go b/webpush/store_postgres.go index 1c9adf0a..84168d89 100644 --- a/webpush/store_postgres.go +++ b/webpush/store_postgres.go @@ -53,6 +53,7 @@ const ( VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (endpoint) DO UPDATE SET key_auth = excluded.key_auth, key_p256dh = excluded.key_p256dh, user_id = excluded.user_id, subscriber_ip = excluded.subscriber_ip, updated_at = excluded.updated_at, warned_at = excluded.warned_at + RETURNING id ` postgresUpdateSubscriptionWarningSentQuery = `UPDATE webpush_subscription SET warned_at = $1 WHERE id = $2` postgresUpdateSubscriptionUpdatedAtQuery = `UPDATE webpush_subscription SET updated_at = $1 WHERE endpoint = $2` diff --git a/webpush/store_sqlite.go b/webpush/store_sqlite.go index fcf49fcf..7677f1ce 100644 --- a/webpush/store_sqlite.go +++ b/webpush/store_sqlite.go @@ -56,8 +56,9 @@ const ( sqliteUpsertSubscriptionQuery = ` INSERT INTO subscription (id, endpoint, key_auth, key_p256dh, user_id, subscriber_ip, updated_at, warned_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (endpoint) + ON CONFLICT (endpoint) DO UPDATE SET key_auth = excluded.key_auth, key_p256dh = excluded.key_p256dh, user_id = excluded.user_id, subscriber_ip = excluded.subscriber_ip, updated_at = excluded.updated_at, warned_at = excluded.warned_at + RETURNING id ` sqliteUpdateSubscriptionWarningSentQuery = `UPDATE subscription SET warned_at = ? WHERE id = ?` sqliteUpdateSubscriptionUpdatedAtQuery = `UPDATE subscription SET updated_at = ? WHERE endpoint = ?`