Для 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
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
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
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.