Dla agentów AI: markdown tej strony — /docs-content-en/errors/handling.md indeks dokumentacji — /llms.txt
Artykuły dokumentacji są obecnie dostępne w języku angielskim.
Retries and error handling in code
Ready-made response handlers in JavaScript, Python, and PHP that distinguish reads from writes and bound the number of retries.
The authoritative delivery and safety table for every temporary code is What is safe to retry. The summary table of all Vibecode API codes is Error codes.
Before using the examples
The caller must pass operationKind explicitly: read for an operation that does
not change data and write for creating, changing, deleting, or sending data. Do
not infer it from the HTTP verb: POST can perform a read.
The examples retry a write automatically only for codes where the previous call
could not create a duplicate. For OPERATION_TIME_LIMIT, BITRIX_TIMEOUT,
BITRIX_UNAVAILABLE, POOL_EXHAUSTED, DB_TRANSIENT, SERVICE_UNAVAILABLE,
BH_APP_STARTING, and BH_APP_TIMEOUT, a write ends with
STATE_VERIFICATION_REQUIRED. The business code must then read the entity and
prove that the effect is absent. There is no universal safe readBack across
entities.
BITRIX_ERROR, TOKEN_REFRESH_FAILED, and ERROR_LOOP_DETECTED are also outside
the retry loop: fix the cause or restore authorization first. Every retry is
bounded by MAX_ATTEMPTS, honors Retry-After, and adds jitter. Save
X-Request-Id for support, but do not copy it into a retry: it is not an
idempotency key.
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);
}
A network failure without the JSON envelope does not say whether a write may have
applied. In JavaScript and Python, catch the transport exception outside the
function and apply the same rule: a read can use bounded backoff, while a write
requires a state check. The PHP example reports this case as
NETWORK_OUTCOME_UNKNOWN.