쿡북
웹훅 수신기와 재시도 예시입니다.접수하고 웹훅으로 받기
접수 한 번, 알림 한 번, 조회 한 번의 흐름입니다. 수신 서버는 원문 본문을 보존해야 서명을 검증할 수 있습니다.
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를 지키는 재시도 — jsasync 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에 곡 수를 곱해 씁니다.