Idempotenz
Nach einem Timeout gefahrlos wiederholen – ohne doppelt zu generieren oder doppelt zu zahlen.
Ein Idempotency-Key ist ein Etikett für eine Arbeit, etwa „das Video für Bestellung 8814 rendern“. Kommt dieselbe etikettierte Anfrage zweimal bei PopNow an, wird die Arbeit nur einmal erledigt.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1Warum du ihn willst
Ein Timeout lässt dich raten: Vielleicht kam die Anfrage nie an, vielleicht kam sie an und nur die Antwort ging verloren. Ohne Schlüssel gibst du entweder auf (und der Nutzer wartet umsonst) oder wiederholst (und generierst und zahlst vielleicht doppelt). Mit Schlüssel ist eine Wiederholung immer sicher: Du bekommst entweder einen neuen Task oder den, der gerade erstellt wurde.
Wie er sich verhält
- Eine Wiederholung mit demselben Schlüssel vom selben API-Schlüssel liefert den ursprünglichen Task in seinem aktuellen Zustand – kein zweiter Task, keine zweite Reservierung, keine zweite Abbuchung.
- Der Schlüssel ist an das Gesendete gebunden:
model, das ganzeinputundcallBackUrl. Feldreihenfolge und Leerraum spielen keine Rolle, echte Änderungen schon. Eine geänderte Anfrage mit altem Schlüssel liefert409; gib ihr einen neuen. - Ein Schlüssel gilt 24 Stunden ab der ersten Verwendung. Danach erzeugt derselbe Schlüssel einen neuen, kostenpflichtigen Task – beende automatische Wiederholungen also deutlich vorher.
- Schlüssel sind bis zu 128 Zeichen lang und müssen nur innerhalb deines Kontos eindeutig sein. Ein anderer API-Schlüssel deines Kontos kann ihn nicht wiederverwenden: Er bekommt
409ohne die ursprüngliche Task-ID.
| Endpunkt | Eine Wiederholung mit demselben Schlüssel … |
|---|---|
POST /api/v1/jobs/createTask | liefert den ursprünglichen Task |
POST /api/v1/jobs/retry | liefert den Task, den dieser Retry erzeugt hat (der Schlüssel gilt pro Quell-Task) |
POST /v1/videos | liefert den ursprünglichen Task |
POST /api/v1/assets | liefert das bereits gespeicherte Asset (hier ist der Header Pflicht) |
Lese-Endpunkte und die Endpunkte für Upload- und Download-Links ignorieren den Header. jobs/purge braucht keinen: Eine Wiederholung ist dort ohnehin harmlos.
Schlüssel wählen
- Ein Schlüssel pro Arbeit, nicht pro HTTP-Versuch. Jede Wiederholung muss ihn wiederverwenden.
- Leite ihn aus etwas ab, das du ohnehin speicherst:
order-8814-render-1lässt sich nach einem Absturz neu berechnen, eine zufällige UUID nur, wenn du sie vorher gespeichert hast. - Nutzt du eine UUID, erzeuge sie vor dem ersten Versuch und speichere sie neben der Arbeit, zu der sie gehört.
Den Schlüssel nie in der Wiederholungsschleife erzeugen
// 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);Konflikte
All diese Fälle liefern code: 409:
| Was passiert ist | Wiederholen? |
|---|---|
| Zwei Anfragen mit demselben Schlüssel kamen gleichzeitig an | Ja – ein paar hundert Millisekunden warten und mit demselben Schlüssel erneut senden |
| Der Schlüssel wurde schon für eine andere Anfrage verwendet | Nein – die Anfrage hat sich geändert und braucht einen neuen Schlüssel |
| Ein anderer API-Schlüssel deines Kontos hat diesen Schlüssel schon benutzt | Nein – mit dem API-Schlüssel wiederholen, der den ersten Versuch gemacht hat |
Dein Code sollte nicht nach msg verzweigen; erlaube also ein, zwei 409-Wiederholungen und reich den Fehler dann an einen Menschen weiter.
Vollständiges Beispiel
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',
);
