A Stripe create that Stripe answered with a server error is retried for a day under a key Stripe will only answer with that error #312

Closed
opened 2026-10-31 01:01:36 +00:00 by cgalo5758 · 1 comment
Owner

What happens

The console sends some writes to Stripe through a queue, the outbox: a row written in the same database transaction as the change that needs it, executed in the background, retried for up to a day, and then set aside as failed ("dead-lettered") for an operator. Today it carries the product and price creates that Sync to Stripe starts on a product's page. Each create carries an idempotency key, a value that tells Stripe two requests are one, so a retry after a lost answer gets the first object back instead of a second one. The key is the same on every attempt.

Stripe saves its first answer to a key once it has started executing the request, a 500 included, and returns that answer to every later request with the key for at least 24 hours. The outbox treats a 500 like a network fault and retries it: about 150 attempts over the day, each answered with the same saved 500, after which the entry is dead-lettered. All that day the product's readiness panel says the price is syncing and "this clears once the sync lands", and the product cannot be bought. After the dead-letter, the panel offers Retry, which puts the same entry back in the queue, so it sends the same key and, while Stripe keeps the key, gets the same 500.

A 500 does not say whether Stripe created the object, so sending the create again under a new key could make a second product or price.

What should happen

A create that Stripe answers with its own 500 stops at once and is dead-lettered with a reason that says the outcome is unknown. Retry creates the object only after the console has found whether Stripe already made it; the console's create puts the product's or price's id in the object's metadata, so an object Stripe made is found and mapped, not created twice. A gateway's error page (502, 503 or 504 without a Stripe error body) is still retried, because Stripe saved nothing for it.

Where

internal/integrations/stripe/workflows/outbox.go (classifyStripeError, outboxIdempotencyKey), internal/integrations/stripe/catalogsync/catalogsync.go (the Retry that puts a dead-lettered entry back in the queue)

Why it matters

After one server error at Stripe, a product cannot be sold for at least a day while the console repeats a request that cannot succeed, and the Retry offered to the operator repeats it again.

### What happens The console sends some writes to Stripe through a queue, the outbox: a row written in the same database transaction as the change that needs it, executed in the background, retried for up to a day, and then set aside as failed ("dead-lettered") for an operator. Today it carries the product and price creates that Sync to Stripe starts on a product's page. Each create carries an idempotency key, a value that tells Stripe two requests are one, so a retry after a lost answer gets the first object back instead of a second one. The key is the same on every attempt. Stripe saves its first answer to a key once it has started executing the request, a 500 included, and returns that answer to every later request with the key for at least 24 hours. The outbox treats a 500 like a network fault and retries it: about 150 attempts over the day, each answered with the same saved 500, after which the entry is dead-lettered. All that day the product's readiness panel says the price is syncing and "this clears once the sync lands", and the product cannot be bought. After the dead-letter, the panel offers Retry, which puts the same entry back in the queue, so it sends the same key and, while Stripe keeps the key, gets the same 500. A 500 does not say whether Stripe created the object, so sending the create again under a new key could make a second product or price. ### What should happen A create that Stripe answers with its own 500 stops at once and is dead-lettered with a reason that says the outcome is unknown. Retry creates the object only after the console has found whether Stripe already made it; the console's create puts the product's or price's id in the object's metadata, so an object Stripe made is found and mapped, not created twice. A gateway's error page (502, 503 or 504 without a Stripe error body) is still retried, because Stripe saved nothing for it. ### Where `internal/integrations/stripe/workflows/outbox.go` (`classifyStripeError`, `outboxIdempotencyKey`), `internal/integrations/stripe/catalogsync/catalogsync.go` (the Retry that puts a dead-lettered entry back in the queue) ### Why it matters After one server error at Stripe, a product cannot be sold for at least a day while the console repeats a request that cannot succeed, and the Retry offered to the operator repeats it again.
cgalo5758 added this to the Public launch milestone 2026-10-31 01:01:36 +00:00
cgalo5758 added the
kind
bug
area/billingarea/integrations
labels 2026-10-31 01:01:36 +00:00
Author
Owner

The outbox is the console's queue of Stripe actions: a row written in the same database transaction as the change that needs it, executed in the background, retried for up to a day, and then set aside as failed (dead-lettered) for an operator. An idempotency key is a value that tells Stripe two requests are one.

A product or price create that Stripe answers with its own server error (Stripe's error object with a status of 500 or above) is now set aside after that one attempt, with the reason "Stripe answered with a server error; whether it created the price is unknown", because Stripe answers every later request with the same idempotency key from the saved error, and a server error does not say whether the object was made. The product's readiness panel shows the failed sync at once instead of "syncing" for a day. A gateway's error page, which carries no Stripe error object, and a request that gets no answer are still retried.

Retry now counts each time it sends an entry back, and the entry's create then carries a new idempotency key. Before that create, the entry lists the products, or the prices of its product, that Stripe made since an hour before the entry was written, through Stripe's list, which shows what Stripe has written, rather than its search, which lags. It maps the oldest one whose metadata carries the console's product or price id, a price also with the same amount, currency and interval, and creates the object only when none does. An entry never sent back keeps the key it always had. Each create also ends with its attempt's two-minute deadline.

After a failed sync, the readiness panel's line ends at the reason, without "Use Retry to run it again."; the second line under the control, which repeated it, is gone; and the control, which read "Retry Stripe sync", now reads "Find or create in Stripe".

Commit: 16aad935

The outbox is the console's queue of Stripe actions: a row written in the same database transaction as the change that needs it, executed in the background, retried for up to a day, and then set aside as failed (dead-lettered) for an operator. An idempotency key is a value that tells Stripe two requests are one. A product or price create that Stripe answers with its own server error (Stripe's error object with a status of 500 or above) is now set aside after that one attempt, with the reason "Stripe answered with a server error; whether it created the price is unknown", because Stripe answers every later request with the same idempotency key from the saved error, and a server error does not say whether the object was made. The product's readiness panel shows the failed sync at once instead of "syncing" for a day. A gateway's error page, which carries no Stripe error object, and a request that gets no answer are still retried. Retry now counts each time it sends an entry back, and the entry's create then carries a new idempotency key. Before that create, the entry lists the products, or the prices of its product, that Stripe made since an hour before the entry was written, through Stripe's list, which shows what Stripe has written, rather than its search, which lags. It maps the oldest one whose metadata carries the console's product or price id, a price also with the same amount, currency and interval, and creates the object only when none does. An entry never sent back keeps the key it always had. Each create also ends with its attempt's two-minute deadline. After a failed sync, the readiness panel's line ends at the reason, without "Use Retry to run it again."; the second line under the control, which repeated it, is gone; and the control, which read "Retry Stripe sync", now reads "Find or create in Stripe". Commit: 16aad935
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wiki-cafe/member-console#312