Для AI-агентов: markdown этой страницы — /docs-content/errors/handling.md индекс документации — /llms.txt

Повторы и обработка ошибок в коде

Готовые обработчики ответа на JavaScript, Python и PHP, которые различают чтение и запись и ограничивают число повторов.

Авторитетная таблица доставки и безопасности для каждого временного кода — Что безопасно повторять. Сводная таблица всех кодов API Вайбкод — Коды ошибок.

Перед использованием примеров

Вызывающий обязан явно передать operationKind: read для операции без изменения данных и write для создания, изменения, удаления или отправки. Не выводите это значение из HTTP-глагола: POST может выполнять чтение.

Примеры автоматически повторяют запись только для кодов, по которым предыдущий вызов не мог создать дубль. Для OPERATION_TIME_LIMIT, BITRIX_TIMEOUT, BITRIX_UNAVAILABLE, POOL_EXHAUSTED, DB_TRANSIENT, SERVICE_UNAVAILABLE, BH_APP_STARTING и BH_APP_TIMEOUT запись завершается ошибкой STATE_VERIFICATION_REQUIRED. После неё бизнес-код должен перечитать сущность и доказать отсутствие эффекта. Общего безопасного readBack для разных сущностей нет.

BITRIX_ERROR, TOKEN_REFRESH_FAILED и ERROR_LOOP_DETECTED также не входят в цикл повтора: сначала нужно исправить причину или восстановить авторизацию. Любой повтор ограничен MAX_ATTEMPTS, учитывает Retry-After и добавляет случайную задержку. X-Request-Id сохраняйте для поддержки, но не копируйте в повторный запрос: это не ключ идемпотентности.

JavaScript

javascript
const MAX_ATTEMPTS = 5;
const SAFE_WRITE_RETRY = new Set([
  'QUEUE_OVERFLOW', 'QUEUE_TIMEOUT', 'RATE_LIMITED',
  'TIMEOUT_QUARANTINE', 'LARGE_BODY_BACKEND_BUSY',
]);
const VERIFY_WRITE_STATE = new Set([
  'OPERATION_TIME_LIMIT', 'BITRIX_TIMEOUT', 'BITRIX_UNAVAILABLE',
  'POOL_EXHAUSTED', 'DB_TRANSIENT', 'SERVICE_UNAVAILABLE', 'BH_APP_STARTING',
  'BH_APP_TIMEOUT',
]);
const FIX_BEFORE_RETRY = new Set([
  'BITRIX_ERROR', 'TOKEN_REFRESH_FAILED', 'ERROR_LOOP_DETECTED',
]);

function retryDelaySeconds(error, response, attempt) {
  const rawHeader = response.headers.get('Retry-After');
  const header = rawHeader === null ? Number.NaN : Number(rawHeader);
  const base = error.retryAfter ?? (Number.isFinite(header) ? header : Math.min(30, 2 ** (attempt - 1)));
  return Math.max(1, base) + Math.random();
}

async function vibeRequest(url, options, operationKind, attempt = 1) {
  if (!['read', 'write'].includes(operationKind)) {
    throw new Error('operationKind must be read or write');
  }

  const response = await fetch(url, {
    ...options,
    headers: {
      'X-Api-Key': process.env.VIBE_API_KEY,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
  const data = await response.json();
  if (data.success) return data;

  const { code, message } = data.error;
  if (operationKind === 'write' && VERIFY_WRITE_STATE.has(code)) {
    throw new Error(`STATE_VERIFICATION_REQUIRED: ${code}`);
  }
  if (FIX_BEFORE_RETRY.has(code)) {
    throw new Error(`FIX_BEFORE_RETRY: ${code}: ${message}`);
  }

  const retryable = SAFE_WRITE_RETRY.has(code)
    || (operationKind === 'read' && VERIFY_WRITE_STATE.has(code));
  if (!retryable) throw new Error(`${code}: ${message}`);
  if (attempt >= MAX_ATTEMPTS) {
    throw new Error(`${code}: still failing after ${MAX_ATTEMPTS} attempts`);
  }

  const wait = retryDelaySeconds(data.error, response, attempt);
  await new Promise(resolve => setTimeout(resolve, wait * 1000));
  return vibeRequest(url, options, operationKind, attempt + 1);
}

Python

Python
import os
import random
import time
import requests

MAX_ATTEMPTS = 5
SAFE_WRITE_RETRY = {
    "QUEUE_OVERFLOW", "QUEUE_TIMEOUT", "RATE_LIMITED",
    "TIMEOUT_QUARANTINE", "LARGE_BODY_BACKEND_BUSY",
}
VERIFY_WRITE_STATE = {
    "OPERATION_TIME_LIMIT", "BITRIX_TIMEOUT", "BITRIX_UNAVAILABLE",
    "POOL_EXHAUSTED", "DB_TRANSIENT", "SERVICE_UNAVAILABLE", "BH_APP_STARTING",
    "BH_APP_TIMEOUT",
}
FIX_BEFORE_RETRY = {
    "BITRIX_ERROR", "TOKEN_REFRESH_FAILED", "ERROR_LOOP_DETECTED",
}

def vibe_request(url, operation_kind, method="GET", json_data=None, attempt=1):
    if operation_kind not in ("read", "write"):
        raise ValueError("operation_kind must be read or write")

    response = requests.request(
        method,
        url,
        headers={"X-Api-Key": os.environ["VIBE_API_KEY"], "Content-Type": "application/json"},
        json=json_data,
    )
    data = response.json()
    if data.get("success"):
        return data

    error = data.get("error", {})
    code = error.get("code", "UNKNOWN")
    message = error.get("message", "Unknown error")
    if operation_kind == "write" and code in VERIFY_WRITE_STATE:
        raise RuntimeError(f"STATE_VERIFICATION_REQUIRED: {code}")
    if code in FIX_BEFORE_RETRY:
        raise RuntimeError(f"FIX_BEFORE_RETRY: {code}: {message}")

    retryable = code in SAFE_WRITE_RETRY or (
        operation_kind == "read" and code in VERIFY_WRITE_STATE
    )
    if not retryable:
        raise RuntimeError(f"{code}: {message}")
    if attempt >= MAX_ATTEMPTS:
        raise RuntimeError(f"{code}: still failing after {MAX_ATTEMPTS} attempts")

    header_delay = response.headers.get("Retry-After")
    base_delay = error.get("retryAfter") or (
        float(header_delay) if header_delay else min(30, 2 ** (attempt - 1))
    )
    time.sleep(max(1, base_delay) + random.random())
    return vibe_request(url, operation_kind, method, json_data, attempt + 1)

PHP

php
const MAX_ATTEMPTS = 5;
const SAFE_WRITE_RETRY = [
    'QUEUE_OVERFLOW', 'QUEUE_TIMEOUT', 'RATE_LIMITED',
    'TIMEOUT_QUARANTINE', 'LARGE_BODY_BACKEND_BUSY',
];
const VERIFY_WRITE_STATE = [
    'OPERATION_TIME_LIMIT', 'BITRIX_TIMEOUT', 'BITRIX_UNAVAILABLE',
    'POOL_EXHAUSTED', 'DB_TRANSIENT', 'SERVICE_UNAVAILABLE', 'BH_APP_STARTING',
    'BH_APP_TIMEOUT',
];
const FIX_BEFORE_RETRY = [
    'BITRIX_ERROR', 'TOKEN_REFRESH_FAILED', 'ERROR_LOOP_DETECTED',
];

function vibeRequest(
    string $url,
    string $operationKind,
    string $method = 'GET',
    ?array $requestData = null,
    int $attempt = 1,
): array {
    if (!in_array($operationKind, ['read', 'write'], true)) {
        throw new InvalidArgumentException('operationKind must be read or write');
    }

    $responseHeaders = [];
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => [
            'X-Api-Key: ' . getenv('VIBE_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_HEADERFUNCTION => function ($curl, string $line) use (&$responseHeaders): int {
            $parts = explode(':', $line, 2);
            if (count($parts) === 2) {
                $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
            }
            return strlen($line);
        },
    ]);
    if ($requestData !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData));
    }
    $rawBody = curl_exec($ch);
    if ($rawBody === false) {
        $transportError = curl_error($ch);
        curl_close($ch);
        throw new RuntimeException("NETWORK_OUTCOME_UNKNOWN: $transportError");
    }
    curl_close($ch);

    $body = json_decode($rawBody, true);
    if (!is_array($body)) throw new RuntimeException('Invalid JSON response');
    if (!empty($body['success'])) return $body;

    $error = $body['error'] ?? [];
    $code = $error['code'] ?? 'UNKNOWN';
    $message = $error['message'] ?? 'Unknown error';
    if ($operationKind === 'write' && in_array($code, VERIFY_WRITE_STATE, true)) {
        throw new RuntimeException("STATE_VERIFICATION_REQUIRED: $code");
    }
    if (in_array($code, FIX_BEFORE_RETRY, true)) {
        throw new RuntimeException("FIX_BEFORE_RETRY: $code: $message");
    }

    $retryable = in_array($code, SAFE_WRITE_RETRY, true)
        || ($operationKind === 'read' && in_array($code, VERIFY_WRITE_STATE, true));
    if (!$retryable) throw new RuntimeException("$code: $message");
    if ($attempt >= MAX_ATTEMPTS) {
        throw new RuntimeException("$code: still failing after " . MAX_ATTEMPTS . ' attempts');
    }

    $baseDelay = $error['retryAfter']
        ?? ($responseHeaders['retry-after'] ?? min(30, 2 ** ($attempt - 1)));
    sleep(max(1, (int) $baseDelay));
    usleep(random_int(0, 1000000));
    return vibeRequest($url, $operationKind, $method, $requestData, $attempt + 1);
}

Сетевой сбой без JSON-конверта не сообщает, успела ли запись примениться. В JavaScript и Python перехватите transport exception снаружи и используйте то же правило: чтение можно повторить с ограниченным backoff, запись требует проверки состояния. PHP-пример возвращает такой случай как NETWORK_OUTCOME_UNKNOWN.

Смотрите также