QuillAIQuillAIDocs
Войти
РуководстваПовторные попытки

Повторные попытки

Повторяйте запрос только при 5xx и сетевых таймаутах — никогда при 4xx. Используйте экспоненциальную задержку с jitter, жёсткий лимит попыток и ключ идемпотентности, чтобы повторы не создавали дубликаты задач.

Когда повторять

Правило простое: повторяйте только если ошибка похожа на временную и сервер не сказал, что запрос неверен. Ответы 4xx детерминированы — тот же запрос приведёт к той же ошибке, повторы только жгут квоту.

СценарийПовторять?Комментарий
2xxНетУспех — повтор не нужен.
4xxНетДетерминированная ошибка клиента. Покажите error.code пользователю — повтор даст тот же результат.
429Да, с задержкойЗарезервировано для будущего rate-limiting. Соблюдайте Retry-After, если есть, иначе подождите минимум 1 секунду и повторите один раз.
5xxДаВременная ошибка сервера. Повторите с экспоненциальной задержкой и jitter, до 5 попыток.
сеть/таймаутДаОбрыв соединения, DNS-ошибка или клиентский таймаут. Повторите — но POST-запросы защищайте ключом идемпотентности (см. ниже).
ошибка парсингаНетНевалидный JSON от сервера почти всегда — баг или прокси, вставляющий HTML. Не зацикливайте повторы — залогируйте и прервите операцию.
transcription.failedНетАсинхронный отказ самой задачи. Требует вмешательства пользователя (починить вход, пополнить баланс). Повтор запроса натолкнётся на ту же ошибку.

Стратегия задержки

Используйте экспоненциальную задержку с потолком 30 секунд: 1с, 2с, 4с, 8с, 16с, 30с, с полным случайным jitter на каждом шаге. Ограничьте 5 повторами и общим дедлайном (в примере ниже — 2 минуты), чтобы сломанный апстрим никогда не подвешивал воркер навсегда.

Не повторяйте без jitter. Если все клиенты откладывают повторы по одному и тому же расписанию, кратковременный сбой превращается в thundering herd — все бьются в сервер в одну секунду. Полный jitter — delay = random(0, exp) — равномерно распределяет нагрузку и стабильно лучше фиксированного или частичного jitter для независимых клиентов.
retry.tstypescript
type RetryOpts = { retries?: number; baseMs?: number; capMs?: number; totalMs?: number };

export async function retry<T>(
  fn: (signal: AbortSignal) => Promise<T>,
  opts: RetryOpts = {},
): Promise<T> {
  const { retries = 5, baseMs = 1000, capMs = 30_000, totalMs = 120_000 } = opts;
  const ctrl = new AbortController();
  const deadline = setTimeout(() => ctrl.abort(), totalMs);
  try {
    let lastErr: unknown;
    for (let attempt = 0; attempt <= retries; attempt++) {
      try {
        return await fn(ctrl.signal);
      } catch (err) {
        lastErr = err;
        if (attempt === retries || !isRetryable(err)) throw err;
        const exp = Math.min(capMs, baseMs * 2 ** attempt);
        const jitter = Math.random() * exp;
        await new Promise((r) => setTimeout(r, jitter));
      }
    }
    throw lastErr;
  } finally {
    clearTimeout(deadline);
  }
}

function isRetryable(err: any) {
  if (err?.name === "AbortError") return false;
  if (err?.status && err.status >= 500) return true;
  return err?.code === "ETIMEDOUT" || err?.code === "ECONNRESET" || err?.name === "TypeError";
}

Ошибки 4xx

Никогда не повторяйте 4xx автоматически. По определению сервер говорит: запрос неверен. Цикл повторов сожжёт квоту, зашумит логи и отложит настоящее исправление.

Покажите error.code и error.message пользователю (для интерактивных сценариев) или залогируйте (для фоновых задач) и остановитесь. Ветвитесь по code, а не по message — сообщения для людей и могут меняться.

Ответы 429

429 зарезервирован для будущего rate-limiting и пока не применяется, но корректный клиент должен уметь его обработать уже сейчас. Если в ответе есть заголовок Retry-After (в секундах), подождите указанное время и повторите один раз. Если заголовка нет — подождите минимум секунду. Не подмешивайте 429 в общую экспоненциальную петлю — это отдельная осознанная пауза.

handle-429.jsjavascript
async function sendWith429(req) {
  const res = await fetch("https://api.quillhub.ai/v1/transcriptions", req);
  if (res.status !== 429) return res;
  const header = res.headers.get("Retry-After");
  const waitSec = header ? Math.max(1, parseInt(header, 10) || 1) : 1;
  await new Promise((r) => setTimeout(r, waitSec * 1000));
  return fetch("https://api.quillhub.ai/v1/transcriptions", req);
}

Дубликаты задач

POST /v1/transcriptions пока не идемпотентен. Если запрос оборвался по таймауту, сервер мог успешно принять задачу — слепой повтор создаст вторую и спишет очки дважды. Серверный дедуп через заголовок Idempotency-Key у нас в планах, а пока — дедуплицируйте на клиенте.

Привязывайте ключ идемпотентности к каждому созданию. Генерируйте UUID на логическую задачу и передавайте его как metadata.idempotency_key в POST. Перед повтором по таймауту вызовите GET /v1/transcriptions и пропустите повтор, если нашли запись с тем же ключом.
idempotent-create.tstypescript
const idempotencyKey = crypto.randomUUID();

async function createOnce(url: string) {
  // Before retrying a timed-out POST, check if the job already landed.
  const existing = await api.get("/v1/transcriptions?limit=20").then((r) =>
    r.data.find((t: any) => t.metadata?.idempotency_key === idempotencyKey)
  );
  if (existing) return existing;

  return api.post("/v1/transcriptions", {
    url,
    metadata: { idempotency_key: idempotencyKey },
  });
}

Доставка вебхуков

Повторы вебхуков мы делаем сами. Если ваш endpoint ответил ошибкой или таймаутом, мы повторяем доставку по фиксированному расписанию — 1 минута, 5 минут, 30 минут, 2 часа, 12 часов — всего до 5 повторов. На принимающей стороне ничего писать не нужно: просто верните 2xx, когда сохраните событие. Подробности — /docs/webhooks.

Отказы транскрипций

Задача в статусе status: "failed" — это не временный сбой, а детерминированный исход: неподдерживаемый источник, битый файл, недостаточно очков. Не повторяйте такие задачи программно — тот же вход даст ту же ошибку. Покажите error.code пользователю, чтобы он устранил причину и отправил заново.