Идемпотентность
Как повторять запрос после таймаута, не генерируя и не оплачивая дважды.
Ключ идемпотентности — это ярлык для одной работы, например «отрендерить видео для заказа 8814». Если один и тот же запрос с ярлыком дойдёт до PopNow дважды, работа будет выполнена один раз.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1Зачем он нужен
Таймаут оставляет в неведении: возможно, запрос так и не дошёл, а возможно, дошёл и потерялся только ответ. Без ключа вы либо сдаётесь (и пользователь ждёт впустую), либо повторяете (и, возможно, генерируете и платите дважды). С ключом повтор всегда безопасен: вы получаете либо новую задачу, либо ту, что была создана мгновение назад.
Как это работает
- Повтор с тем же ключом от того же API-ключа возвращает исходную задачу в её текущем состоянии — без второй задачи, второго резерва и второго списания.
- Ключ привязан к тому, что вы отправили:
model, весьinputиcallBackUrl. Порядок полей и пробелы не важны, а любое реальное изменение — важно. Изменённый запрос со старым ключом получит409; дайте ему новый ключ. - Ключ запоминается на 24 часа с первого использования. После этого тот же ключ создаст новую платную задачу, поэтому автоматические повторы прекращайте задолго до этого.
- Ключ — до 128 символов и должен быть уникален только в пределах вашего аккаунта. Другой API-ключ вашего аккаунта не может его переиспользовать: он получит
409без ID исходной задачи.
| Эндпоинт | Повтор с тем же ключом… |
|---|---|
POST /api/v1/jobs/createTask | возвращает исходную задачу |
POST /api/v1/jobs/retry | возвращает задачу, созданную этим повтором (ключ действует в рамках исходной задачи) |
POST /v1/videos | возвращает исходную задачу |
POST /api/v1/assets | возвращает уже сохранённый ассет (здесь заголовок обязателен) |
Эндпоинты чтения и выдачи ссылок на загрузку и скачивание заголовок игнорируют. jobs/purge в нём не нуждается: повторный вызов и так безвреден.
Как выбирать ключи
- Один ключ на одну работу, а не на каждую HTTP-попытку. Каждый повтор должен использовать его снова.
- Выводите ключ из того, что у вас уже хранится:
order-8814-render-1можно пересчитать после сбоя, а случайный UUID — нет, если вы его заранее не сохранили. - Если используете UUID, генерируйте его до первой попытки и храните рядом с работой, к которой он относится.
Никогда не создавайте ключ внутри цикла повторов
// 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);Конфликты
Во всех этих случаях возвращается code: 409:
| Что произошло | Повторять? |
|---|---|
| Два запроса с одним ключом пришли одновременно | Да — подождите несколько сотен миллисекунд и отправьте с тем же ключом |
| Ключ уже использован для другого запроса | Нет — запрос изменился, нужен новый ключ |
| Другой API-ключ вашего аккаунта уже использовал этот ключ | Нет — повторите с тем API-ключом, который делал первую попытку |
Ваш код не должен ветвиться по msg, поэтому допускайте один-два повтора при 409, а затем передавайте ошибку человеку.
Полный пример
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',
);
