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

Сводка стратегий повтора по кодам временных отказов и готовые обработчики ответа на JavaScript, Python и PHP.

Сводная таблица всех кодов API Вайбкод — [Коды ошибок](/docs/errors).

## Повторы и паузы

Сводка по кодам, которые сигнализируют о временной проблеме и подразумевают повтор:

| Код | HTTP | Стратегия повтора |
|-----|------|--------------------|
| [`RATE_LIMITED`](/docs/errors/limits#rate_limited-429), [`QUEUE_OVERFLOW`](/docs/errors/limits#queue_overflow-429), [`QUEUE_TIMEOUT`](/docs/errors/limits#queue_timeout-429) | 429 | Повтор через `Retry-After`, дальше — с растущей паузой и случайной добавкой к ней |
| `LARGE_BODY_BACKEND_BUSY` | 429 | Повтор через `Retry-After` со случайной добавкой к паузе. Запрос не выполнялся и состояние не менял — повтор безопасен и для записи |
| [`OPERATION_TIME_LIMIT`](/docs/errors/limits#operation_time_limit-429) | 429 | Повтор строго через `Retry-After`: раньше этого срока тот же вызов не пройдёт. `scope: "apiKey"` — пауза только на вызывающем ключе |
| [`TIMEOUT_QUARANTINE`](/docs/errors/limits#timeout_quarantine-429) | 429 | Повтор через `Retry-After` со случайной добавкой к паузе, и **не сокращая интервал**: досрочный повтор продлевает паузу. `scope: "portal"` — пауза общая для всех ключей портала |
| [`ERROR_LOOP_DETECTED`](/docs/errors/limits#error_loop_detected-429) | 429 | Повтор через `Retry-After`, поля `retryAfter` в теле нет. Сначала разберите причину: код означает серию одинаковых отказов на одном ключе. Каждый N-й повтор платформа пропускает как пробу восстановления |
| [`BITRIX_TIMEOUT`](/docs/errors/platform#bitrix_timeout-503) | 503 | Более длинная пауза перед повтором, чем при 429. Для записи — сначала перечитайте сущность: изменение могло уже примениться |
| [`BITRIX_UNAVAILABLE`](/docs/errors/platform#bitrix_unavailable-502) | 502 | Это 5xx самого Битрикс24 или сетевая проблема между Вайбкод и порталом, а не перегрузка очереди: заголовка `Retry-After` в ответе нет, повторяйте с экспоненциальной задержкой. Для записи — сначала проверьте, не применилась ли она |
| `POOL_EXHAUSTED`, `DB_TRANSIENT`, `SERVICE_UNAVAILABLE` | 503 | Повтор через `retryAfter` (он же заголовок `Retry-After`) — несколько секунд. У `DB_TRANSIENT` транзакция откачена целиком, поэтому повтор безопасен и для записи. У `SERVICE_UNAVAILABLE` исход мог остаться неизвестным — для не-идемпотентных операций сверьте состояние сущности перед повтором |
| [`INTERNAL_ERROR`](/docs/errors/platform#internal_error-500) | 500 | Повторить запрос. Стабильно воспроизводимая ошибка повтором не лечится — отправьте обращение с временем запроса и заголовком `X-Request-Id` |

Таблица покрывает отказы, которые приходят на любом эндпоинте. Остальные временные состояния — установку приложения через модуль-коннектор, ожидание обновления Битрикс24, заморозку ключа на время удаления аккаунта — описывает строка кода в [сводной таблице](/docs/errors): там сказано, меняет ли что-нибудь повтор.

## Обработка ошибок в коде

Обработчики ниже ограничивают число попыток. Ограничение обязательно: отказ, который держится дольше вашего терпения, при бесконечном повторе превращается в серию одинаковых запросов, и платформа закрывает её кодом [`ERROR_LOOP_DETECTED`](/docs/errors/limits#error_loop_detected-429).

### JavaScript

```javascript
const MAX_ATTEMPTS = 5;

async function vibeRequest(url, options = {}, attempt = 1) {
  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) {
    const { code, message, retryAfter } = data.error;
    const canRetry = attempt < MAX_ATTEMPTS;

    switch (code) {
      case 'RATE_LIMITED':
      case 'QUEUE_TIMEOUT': {
        if (!canRetry) throw new Error(`${code}: отказ держится ${MAX_ATTEMPTS} попыток подряд`);
        const wait = retryAfter ?? Number(response.headers.get('Retry-After') ?? 1);
        await new Promise(r => setTimeout(r, wait * 1000));
        return vibeRequest(url, options, attempt + 1);
      }

      case 'BITRIX_UNAVAILABLE':
        if (!canRetry) throw new Error(`${code}: отказ держится ${MAX_ATTEMPTS} попыток подряд`);
        await new Promise(r => setTimeout(r, 5000));
        return vibeRequest(url, options, attempt + 1);

      case 'MISSING_API_KEY':
      case 'INVALID_API_KEY':
        throw new Error('Проверьте API-ключ');

      default:
        throw new Error(`${code}: ${message}`);
    }
  }

  return data;
}
```

### Python

```python
import os
import time
import requests

MAX_ATTEMPTS = 5

def vibe_request(url, method="GET", json_data=None, attempt=1):
    headers = {
        "X-Api-Key": os.environ["VIBE_API_KEY"],
        "Content-Type": "application/json",
    }

    response = requests.request(method, url, headers=headers, json=json_data)
    data = response.json()

    if not data.get("success"):
        err = data.get("error", {})
        code = err.get("code")
        message = err.get("message")
        retry_after = err.get("retryAfter") or int(response.headers.get("Retry-After", 1))
        can_retry = attempt < MAX_ATTEMPTS

        if code in ("RATE_LIMITED", "QUEUE_TIMEOUT"):
            if not can_retry:
                raise Exception(f"{code}: отказ держится {MAX_ATTEMPTS} попыток подряд")
            time.sleep(retry_after)
            return vibe_request(url, method, json_data, attempt + 1)

        if code == "BITRIX_UNAVAILABLE":
            if not can_retry:
                raise Exception(f"{code}: отказ держится {MAX_ATTEMPTS} попыток подряд")
            time.sleep(5)
            return vibe_request(url, method, json_data, attempt + 1)

        raise Exception(f"{code}: {message}")

    return data
```

### PHP

```php
const MAX_ATTEMPTS = 5;

function vibeRequest(string $url, string $method = 'GET', ?array $data = null, int $attempt = 1): array {
    $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',
        ],
    ]);
    if ($data !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    }
    $body = json_decode(curl_exec($ch), true);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === null) {
        throw new Exception("HTTP error: $httpCode");
    }

    if (empty($body['success'])) {
        $errCode = $body['error']['code'] ?? 'UNKNOWN';
        $errMsg = $body['error']['message'] ?? 'Unknown error';
        $retryAfter = $body['error']['retryAfter'] ?? 1;
        $canRetry = $attempt < MAX_ATTEMPTS;

        if (in_array($errCode, ['RATE_LIMITED', 'QUEUE_TIMEOUT'], true)) {
            if (!$canRetry) {
                throw new Exception("$errCode: отказ держится " . MAX_ATTEMPTS . ' попыток подряд');
            }
            sleep((int) $retryAfter);
            return vibeRequest($url, $method, $data, $attempt + 1);
        }

        if ($errCode === 'BITRIX_UNAVAILABLE') {
            if (!$canRetry) {
                throw new Exception("$errCode: отказ держится " . MAX_ATTEMPTS . ' попыток подряд');
            }
            sleep(5);
            return vibeRequest($url, $method, $data, $attempt + 1);
        }

        throw new Exception("$errCode: $errMsg");
    }

    return $body;
}
```

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

- [Коды ошибок](/docs/errors)
- [Лимиты, очереди и паузы](/docs/errors/limits)
- [Битрикс24 и платформа](/docs/errors/platform)
- [Лимиты и оптимизация](/docs/optimization)
- [Batch](/docs/batch)
- [CLI и cURL](/docs/cli)
