쿡북

웹훅 수신기와 재시도 예시입니다.

접수하고 웹훅으로 받기

접수 한 번, 알림 한 번, 조회 한 번의 흐름입니다. 수신 서버는 원문 본문을 보존해야 서명을 검증할 수 있습니다.
import express from "express";
import { verifyWebhook } from "./verify.js"; // 웹훅 문서의 서명 검증 함수
​
const app = express();
const seen = new Set();
​
app.post("/hooks/mellowz", express.raw({ type: "application/json" }), async (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyWebhook(process.env.WEBHOOK_SECRET, req.headers, raw)) return res.sendStatus(401);
const event = JSON.parse(raw);
if (seen.has(event.id)) return res.sendStatus(200);
seen.add(event.id);
res.sendStatus(200);
// 응답 뒤에 결과를 조회한다.
const kind = { image: "images", video: "videos", audio: "audio" }[event.data.kind];
const job = await fetch(`${process.env.MELLOWZ_STUDIO_URL}/v1/${kind}/${event.data.jobId}`, {
headers: { Authorization: `Bearer ${process.env.MELLOWZ_STUDIO_TOKEN}` },
}).then((r) => r.json());
console.log(event.type, job);
});
실제 서비스에서는 seen을 메모리 대신 DB에 두고, 조회는 큐로 넘기세요.

웹훅 없이 기다리기

외부에서 수신 주소를 열 수 없을 때는 폴링합니다. 작업 기다리기의 루프를 쓰고, poll_after_seconds를 지킵니다.

429를 만났을 때 재시도

Retry-After를 지키는 재시도 — js
async function withRetry(send) {
for (let attempt = 0; attempt < 5; attempt += 1) {
const res = await send();
if (res.status !== 429) return res;
const body = await res.clone().json();
if (body.error.code !== "RATE_LIMITED") return res; // 지출 한도(SPEND_LIMIT_EXCEEDED)는 기다려도 풀리지 않을 수 있다
// 동시 작업 한도(image_active 등)는 Retry-After 가 없다. 5초 뒤 같은 키로 다시 보낸다.
const wait = Number(res.headers.get("Retry-After") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
}
throw new Error("재시도 횟수를 넘었습니다");
}

접수 전에 모델 읽기

접수 전에 GET /v1/models/image로 모델의 옵션 값과 가격을 읽어, 지원하지 않는 값을 보내 400을 받는 일을 피합니다. 응답의 price.cherry를 maxCherry로 쓰면 상한이 보이는 가격과 같아집니다. 영상은 /v1/models/video의 cherry_quotes, 오디오는 /v1/models/audio의 cherry_per_track에 곡 수를 곱해 씁니다.

관련