Idempotenza
Ritentare dopo un timeout senza generare due volte né pagare due volte.
Una chiave di idempotenza è un’etichetta che metti su un lavoro, come «renderizzare il video dell’ordine 8814». Se la stessa richiesta etichettata arriva due volte a PopNow, il lavoro viene fatto una volta sola.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1Perché ti serve
Un timeout ti lascia nel dubbio: forse la richiesta non è mai arrivata, forse è arrivata e si è persa solo la risposta. Senza chiave puoi solo arrenderti (e l’utente aspetta per niente) o ritentare (e magari generare e pagare due volte). Con la chiave ritentare è sempre sicuro: ottieni una nuova attività oppure quella creata un attimo prima.
Come si comporta
- Un nuovo invio con la stessa chiave, dalla stessa chiave API, restituisce l’attività originale nel suo stato attuale: nessuna seconda attività, trattenuta o addebito.
- La chiave è legata a ciò che hai inviato:
model, tutto l’inputecallBackUrl. Ordine dei campi e spazi non contano, ma ogni modifica reale sì. Una richiesta modificata con una vecchia chiave restituisce409; dalle una chiave nuova. - Una chiave viene ricordata per 24 ore dal primo utilizzo. Dopo, la stessa chiave crea una nuova attività a pagamento, quindi ferma i tentativi automatici molto prima.
- Le chiavi arrivano a 128 caratteri e devono essere uniche solo nel tuo account. Un’altra chiave API del tuo account non può riusarla: riceve
409senza l’ID dell’attività originale.
| Endpoint | Un nuovo invio con la stessa chiave… |
|---|---|
POST /api/v1/jobs/createTask | restituisce l’attività originale |
POST /api/v1/jobs/retry | restituisce l’attività creata da quel retry (la chiave vale per l’attività di origine) |
POST /v1/videos | restituisce l’attività originale |
POST /api/v1/assets | restituisce l’asset già salvato (qui l’header è obbligatorio) |
Gli endpoint di lettura e quelli che emettono link di caricamento o download ignorano l’header. jobs/purge non ne ha bisogno: ripeterlo è già innocuo.
Scegliere le chiavi
- Una chiave per lavoro, non per tentativo HTTP. Ogni nuovo tentativo deve riusarla.
- Ricavala da qualcosa che conservi già:
order-8814-render-1si può ricalcolare dopo un crash; un UUID casuale no, a meno che tu non l’abbia salvato prima. - Se usi un UUID, generalo prima del primo tentativo e conservalo accanto al lavoro a cui appartiene.
Non generare mai la chiave dentro il ciclo dei tentativi
// Wrong: a new key per attempt defeats the whole mechanism
for (let i = 0; i < 3; i++) await createTask(body, crypto.randomUUID());
// Right: one key, created once
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) await createTask(body, key);Conflitti
Tutti questi casi restituiscono code: 409:
| Cosa è successo | Ritentare? |
|---|---|
| Due richieste con la stessa chiave sono arrivate insieme | Sì — aspetta qualche centinaio di millisecondi e reinvia con la stessa chiave |
| La chiave è già stata usata per un’altra richiesta | No — la richiesta è cambiata e serve una chiave nuova |
| Un’altra chiave API del tuo account ha già usato questa chiave | No — ritenta con la chiave API che ha fatto il primo tentativo |
Il tuo codice non deve decidere in base a msg: concedi uno o due tentativi su 409 e poi mostra l’errore a una persona.
Esempio completo
const RETRYABLE = new Set([429, 500, 503, 50301]); // business codes from the body
async function createTaskOnce(body, idempotencyKey) {
for (let attempt = 0; attempt < 4; attempt++) {
try {
const res = await fetch('https://api.popnow.ai/api/v1/jobs/createTask', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POPNOW_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(body),
});
const json = await res.json();
if (json.code === 200) return json.data; // a new task, or the original one
if (!RETRYABLE.has(json.code)) throw new Error(`${json.code} ${json.msg} (${json.request_id})`);
} catch (err) {
if (!(err instanceof TypeError)) throw err; // network error: safe to retry with the same key
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 500 + Math.random() * 300));
}
throw new Error('createTask did not succeed after 4 attempts');
}
const task = await createTaskOnce(
{ model: 'alibaba/z-image-turbo/text-to-image', input: { prompt: 'A paper boat on a calm lake' } },
'order-8814-render-1',
);
