spicyapi文件
主要內容

冪等鍵

網路逾時之後安全重試,不重複生成、不重複扣費。

createTask 支援 Idempotency-Key 請求標頭。同一個帳號、同一把 API Key 用同一個鍵重複提交,回傳的是首次建立的那個任務:不重複排隊,也不重複凍結金額或收費。

POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1

為什麼必須用它

網路逾時時你處在最壞的一種不確定裡:請求可能沒到、也可能到了但回應遺失了。不帶冪等鍵的話你只有兩個都不好的選擇——不重試(使用者白等),或者重試(可能生成兩次、扣兩次錢)。

帶上冪等鍵,重試就是安全的:要麼建立,要麼把上次建立的那個任務還給你。

只有會建立新任務的介面讀取這個標頭

createTaskretry 都支援 Idempotency-Keyretry 會把它限定在「來源任務 + retry 操作」的語義內,因此不同來源任務之間不會衝突。查詢和簽發臨時憑證的介面不讀這個標頭。

24 小時時間窗

一個鍵在 24 小時內有效。過了這個時間窗,同一個鍵會被當成一個新請求,建立一個新任務。

24 小時是兩件事的折衷:用戶端的重試通常在幾秒到幾分鐘內完成,時間窗夠長就行;而時間窗越長,同一個鍵就越久不能重複使用——用固定鍵做日常呼叫的話,你會發現自己一天只能提交一次任務。

鍵怎麼取

  • 每個「邏輯請求」一個鍵,不是每次 HTTP 呼叫一個。重試時必須沿用同一個鍵,否則等於沒用。
  • 用你自己業務裡已有的唯一識別碼:訂單編號 + 用途 + 序號(order-8814-render-1)比隨機 UUID 更好用——程式重啟後你還能算出同一個鍵。
  • 用不上業務識別碼時,在發起第一次請求之前產生一個 UUID 並持久儲存,重試時讀取同一個值。
  • 鍵按帳號佔用,不需要全域唯一;但一次邏輯請求的所有重試還必須使用同一把 API Key。長度上限 128 字元。

同帳號的另一把 API Key 也不能重放

冪等鍵對應按帳號佔用,任務內容則按 API Key 隔離。同帳號的其他 Key 使用已佔用的冪等鍵會得到 409 Conflict,回應不會包含原任務 ID。這樣一把 Key 不能借冪等重放窺探另一把 Key 的任務。

別把亂數寫在重試迴圈裡面

// 錯的:每次重試都換了鍵,等於沒開冪等
for (let i = 0; i < 3; i++) {
  await create({ 'Idempotency-Key': crypto.randomUUID() });
}

// 對的:鍵在迴圈外面產生
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) {
  await create({ 'Idempotency-Key': key });
}

命中重複時會看到什麼

回應回傳同一個 taskIdestimatedCost,但 state 是原任務的目前狀態,可能是 queuedrunning 或終態。重放不會再建立任務、重複凍結或重複收費。

沒有額外的標頭或欄位告訴你「這次是命中了冪等」。要讀取完整任務記錄,拿 taskId 去查 recordInfo

並行提交同一個鍵

同一把 API Key 的兩個請求帶著同一個鍵同時到達時,只有一個會真正建立任務,另一個會拿到同一個 taskId。判定靠資料庫唯一約束而不是「先查再插」——後者在並行下兩次查詢都會回傳空,於是兩個任務都被建立、兩筆凍結都成立。

極少數情況下會回傳:

{ "code": 409, "msg": "Idempotency-Key is already in use by this account" }

這表示鍵被佔用但對應的任務查不到。退避幾百毫秒重試同一個鍵即可。

完整範例

JavaScript
import crypto from 'node:crypto';

async function createTaskWithRetry(payload, { attempts = 4 } = {}) {
  // 鍵在迴圈外面。這是整段程式碼的重點。
  const idempotencyKey = crypto.randomUUID();
  // 這裡判斷的是回應本文中的業務碼,不是 HTTP 狀態碼。
  const retryable = new Set([409, 429, 500, 50301]);

  for (let i = 0; i < attempts; i++) {
    let body;
    try {
      const res = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey,
        },
        body: JSON.stringify(payload),
      });
      body = await res.json();
    } catch {
      // 連線層失敗:請求可能已經到達。帶著同一個鍵重試是安全的。
      await sleep(2 ** i * 500 + Math.random() * 300);
      continue;
    }

    if (res.status === 202 && body.code === 200) return body.data;
    if (!retryable.has(body.code)) {
      throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
    }
    await sleep(2 ** i * 500 + Math.random() * 300);
  }
  throw new Error('重試次數已用盡');
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
Python
import os, random, time, uuid, requests

# 這裡判斷的是回應本文中的業務碼,不是 HTTP 狀態碼。
RETRYABLE = {409, 429, 500, 50301}


def create_task_with_retry(payload: dict, attempts: int = 4) -> dict:
    # 鍵在迴圈外面。這是整段程式碼的重點。
    idempotency_key = str(uuid.uuid4())

    for i in range(attempts):
        try:
            res = requests.post(
                "https://api.spicyapi.ai/api/v1/jobs/createTask",
                headers={
                    "Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
                    "Content-Type": "application/json",
                    "Idempotency-Key": idempotency_key,
                },
                json=payload,
                timeout=30,
            )
            body = res.json()
        except requests.RequestException:
            # 連線層失敗:請求可能已經到達。帶著同一個鍵重試是安全的。
            time.sleep(2**i * 0.5 + random.random() * 0.3)
            continue

        if res.status_code == 202 and body["code"] == 200:
            return body["data"]
        if body["code"] not in RETRYABLE:
            raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
        time.sleep(2**i * 0.5 + random.random() * 0.3)

    raise RuntimeError("重試次數已用盡")

連線層失敗也要帶同一個鍵重試

fetch 拋出例外、連線逾時、讀取逾時——這些情況下你不知道請求有沒有到達。這正是冪等鍵存在的意義:帶著同一個鍵重試,最壞情況也只是拿回上一次建立的任務。

本頁目錄