codingjoe / codingjoe/relay

Webhook webhook-id changes on every retry, so receivers cannot deduplicate

Open
#226 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Python
Stars
4
Forks
0
Avg merge
8h 24m
Merged PRs (30d)
115

Description

## Problem

`deliver_to_webhook` (`services/email/mta/tasks.py:182`) mints a new id on every
call:

```python
msg_id = f"msg_{uuid.uuid7()}"
timestamp = int(time.time())
signature = webhook.sign(msg_id, timestamp, payload_bytes)
```

`deliver_webhook` (`services/email/mta/tasks.py:94`) calls it once per attempt,
and `webhook_retry` (`services/email/mta/tasks.py:56`) retries up to 10 times
over roughly three days (`WEBHOOK_RETRY_DELAYS`,
`services/email/mta/tasks.py:35`). The Standard Webhooks spec defines
`webhook-id` as the idempotency key for exactly this case: a receiver that
processed an attempt but did not manage to acknowledge it (timeout, 5xx after
commit, crash) is supposed to recognise the next attempt by its id. Relay
rotates both the id and the signature per attempt, so the receiver has nothing
to deduplicate on and cannot tell a retry from a new event.

## Impact

Every ambiguous failure becomes a duplicate event on the receiver, and relay
cannot honestly claim at-least-once delivery, because the dedup key it hands out
is unique per attempt rather than per event. The receiver side is the party that
pays for it.

## Suggested fix

Keep the id stable for one logical delivery while keeping the timestamp fresh:

- Derive it deterministically (`msg__`) or generate
it once and store it (on `WebhookDelivery`, or on the message/webhook pair).
- Reuse that id on every attempt and sign each attempt with a new timestamp.
The timestamp is part of the signed content and may change; the id must not.
- Decide whether a manual re-notification of the same message should reuse the
id (receiver deduplicates) or start a new logical delivery (receiver sees a
second event), because that decides whether the id can be derived or has to be
stored with an attempt counter.
- Align `WebhookDelivery` row identity with the id. One row per attempt is fine
for auditing, but the row and the header should agree on what "one delivery"
means.

## Related

- The inbound half of the duplicate delivery issue filed alongside this one.
- `docs/docs/receiving.md` documents the webhook contract and the retry
schedule.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.