A Stripe webhook event is dispatched again when its final status update fails #267
Open
opened 2026-10-24 06:52:47 +00:00 by cgalo5758
·
0 comments
Labels
Clear labels
accessibility
area/billing
area/catalog
area/discourse
area/domains
area/entitlements
area/fedwiki
area/identity
area/integrations
area/licensing
area/member-ui
area/meta
area/operator-ui
area/ops
area/testing
duplicate
good-first-issue
invalid
privacy
security
upstream
wontfix
A barrier for people using assistive technology or a keyboard alone.
The Stripe mirror, checkout, subscriptions, invoices, fulfillment.
Products, prices, plan ladders, purchasability.
The Discourse integration.
The domains registry, claims, placements, the certificate ask.
Entitlement sets, rules, grants, pools, provisioning.
The Federated Wiki integration and farm sync.
Sign-in, sessions, persons, organizations, workspaces, roles.
The provider registry, outbox and webhooks in general.
Licenses, the contributor agreement, SPDX headers.
Member pages.
The repository itself, its contributing guide, CI, the tracker and the workflow.
Operator pages, forms, lists, the design system.
Deployment, configuration, migrations, workflows, instance settings.
The test stack, screens, lint, walkthroughs.
Closed because another issue already covers it.
Small, self-contained, and explained enough to be a first contribution.
Closed because it is not a ticket for this repository.
Touches what a person's data reveals.
Touches authentication, authorization, secrets or data exposure.
Waits on another repository or project before it can move.
Closed because it will not be done, with the reason in the last comment.
kind
bug
The software does something other than what it promises; closed when it again does what it promises.
kind
debt
Code, tests or tooling to clean up with nothing visible changing; closed when they are cleaner.
kind
design
A question to settle before work can be defined; closed when the decision is written down.
kind
docs
Documentation that is wrong or missing; closed when it says the right thing.
kind
enhancement
Something the software does not do yet; closed when it does.
priority
critical
Blocks the active milestone or harms members now.
priority
high
Next in line inside the active milestone.
priority
low
Inside the active milestone, when nothing else is left.
priority
medium
Inside the active milestone, after the high ones.
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: wiki-cafe/member-console#267
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What happens
ProcessWebhookEvent(internal/integrations/stripe/workflows/webhook.go) handles one Stripe event in three steps that do not share a transaction: an update that marks the rowprocessing(only fromreceived,processingorfailed), the dispatch to the handler for the event's type, and an update that marks the rowcompletedorskipped. When the dispatch succeeds and that last update fails (a dropped connection, a database restart), or the worker dies between the two, the attempt fails and Temporal retries the activity. The row is stillprocessing, the first update accepts it, and the handler runs a second time.The impact is low. Each handler tolerates a second run of the same event, and no page shows a difference:
checkout.session.completedandcustomer.subscription.*callfulfillment.ReconcileSubscription, which reads the subscription from Stripe again and converges the console's rows to it. It records a subscription change only when the status differs, and it confers, ends or resumes nothing the first run already did.invoice.finalized,invoice.paid,invoice.payment_failedandinvoice.voidedcallReconcileInvoice, which reads the invoice back and inserts only the lines and charges the console does not hold yet; the invoice sweep runs the same reconcile on a schedule.customer.*,product.*andprice.*write the same values to the same mapping row, which moves itsupdated_at, and skip the event when a newer event for the same object has completed.payment_method.detachedsetspayment_methods.detached_atagain, to the time of the second run; no query reads that column.payment_method.attachedandpayment_method.updatedwrite the card's brand, last four digits, expiry and funding from the event's payload again, and link the payments that name no method.The second run costs one more round of Stripe API reads for the subscription and invoice events. One case can leave stored details older than Stripe's: the payment-method handlers do not check for a newer event about the same card, so when a newer
payment_method.updatedfor that card completes between the two runs, the second run writes the older event's card details over the newer ones until the card's next event.What should happen
A retry of an attempt whose dispatch already finished does not run the handler again: for example, the final status update is retried on its own before the attempt reports failure, or the row records that the dispatch finished so the retry only completes the row. The payment-method handlers also skip an event that a newer completed event for the same payment method supersedes, as the customer, product and price handlers do.
Where
internal/integrations/stripe/workflows/webhook.go(ProcessWebhookEvent)Steps
ProcessWebhookEventfor aprice.updatedevent on a database handle whose statement after the dispatch fails.processing, and the handler writes the price mapping a second time.Why it matters
Every handler happens to tolerate a second run today, so the cost is extra Stripe reads and touched rows, but a handler that is not idempotent would act twice with nothing in the activity to stop it.