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

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);
}

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.

See also