[Bug]: A provider retry that rotates its delivery id defeats ingress dedup and re-sends the message
An ingress route deduplicates on a provider-supplied delivery id (signature.dedupHeader, or webhook-id for standard-webhooks). A provider that mints a fresh id for each retry attempt therefore defeats the dedup entirely: every attempt looks like a new delivery, the plugin runs again, and the recipient gets the message twice.
supabase/auth does exactly that. At v2.197.0, internal/hooks/hookshttp/hookshttp.go:130 opens the retry loop, :136 mints msgID := uuid.Must(uuid.NewV4()) and :137 a fresh currentTime inside it, and :153-154 sets them as webhook-id and webhook-timestamp. So a retried Send SMS hook carries a new id, a new timestamp and a new signature over identical content.
How a user sees it. supabase-otp-hook declares mode: async, and ingress.service.ts enqueues the send before returning the ack (void this.deps.enqueue(...) at :211, ack returned at :223). If the first attempt's response is lost or auth's 5 s hook deadline expires after the gateway already enqueued, auth retries (:167, on a net timeout or any non-final attempt) and the contact receives a second WhatsApp message carrying the same code. Nothing in the gateway can currently collapse it.
Why the existing fallback does not help. deriveDeliveryId already hashes pluginId + instanceId + route + rawBody when the provider sends no dedup header, which would collapse these retries exactly. It is only consulted when the header is absent, so a provider that sends a rotating id is trusted over content that is byte-identical.
The tension, which is why this is not an obvious one-line fix. Switching such a route to body-hash dedup would also collapse a legitimate second request that happens to carry the same bytes. For this provider that means a user asking for a new code while the previous one is still valid: gotrue may reuse an unexpired OTP, in which case the payload repeats and the resend the user asked for would be suppressed. Any fix needs to separate "the same delivery, retried" from "the same content, requested again", and a time window is the obvious but imperfect discriminator.
Possible directions, none of them obviously right:
- Let a route opt into content dedup (
dedupOn: "body"), leaving the choice with the plugin author who knows the provider. - Keep header dedup but add a short secondary window keyed on the payload hash, which the schema already stores (
payloadHash,ingress.service.ts:178). - Do nothing in the gateway and document that at-least-once delivery is the contract, which is defensible but leaves every such plugin exposed.
Filing this so the exposure is tracked rather than rediscovered. It predates and is independent of #1637.
Source: rmyndharis/OpenWA