Обзор
Система состоит из нескольких компонентов, работающих вместе, чтобы предотвратить превышение лимитов API и обеспечить стабильную работу.
┌─────────────────────────────────────────────┐
│ RestrictionManager │
│ (Coordinator of all types of restrictions) │
└─────┬──────────────┬──────────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌────────────┐ ┌─────────────┐
│ Rate │ │ Operating │ │ Adaptive │
│ Limiter │ │ Limiter │ │ Delayer │
└──────────┘ └────────────┘ └─────────────┘
Интерфейсы и типы
ILimiter (базовый интерфейс)
interface ILimiter {
getTitle(): string
setConfig(config: any): Promise<void>
setLogger(logger: LoggerInterface): void
getLogger(): LoggerInterface
canProceed(requestId: string, method: string, params?: any): Promise<boolean>
waitIfNeeded(requestId: string, method: string, params?: any): Promise<number>
updateStats(requestId: string, method: string, data: any): Promise<void>
reset(): Promise<void>
getStats(): Record<string, any>
}
RestrictionParams
interface RestrictionParams {
rateLimit?: RateLimitConfig // Настройки rate limiting
operatingLimit?: OperatingLimitConfig // Настройки лимита времени операций
adaptiveConfig?: AdaptiveConfig // Настройки адаптивной задержки
maxRetries?: number // Максимальное число повторов запроса (по умолчанию: 3). Кроме `batch`-запросов
retryDelay?: number // Базовая задержка между повторами в мс (по умолчанию: 1000)
retryOnNetworkError?: boolean // Повтор при `NETWORK_ERROR` / `REQUEST_TIMEOUT` (HTTP 408) (по умолчанию: true).
// Установите `false` для неидемпотентных вызовов — см. «Неидемпотентные вызовы» ниже.
hardErrorCodes?: string[] // Доп. коды для немедленного выброса (объединяются со встроенным hard-списком).
softErrorCodes?: string[] // Доп. коды для возврата как AjaxResult с ошибкой (объединяются со встроенным soft-списком).
}
Компоненты системы
1. RateLimiter (ограничение частоты)
Назначение: реализует алгоритм «Leaky Bucket» для ограничения частоты запросов.
RateLimiter — конфигурация
interface RateLimitConfig {
burstLimit: number // Ёмкость бакета (максимальное число одновременных запросов)
drainRate: number // Скорость утечки (запросов в секунду)
adaptiveEnabled: boolean // Включить адаптивное управление
}
RateLimiter — принцип работы
- Токеновая система: каждый запрос потребляет один токен
- Автоматическое пополнение: токены пополняются со скоростью
drainRate - Адаптивное управление:
- При частых ошибках лимиты автоматически снижаются на 20%
- При стабильной работе лимиты постепенно восстанавливаются
- Минимальные значения:
drainRate = 0.5,burstLimit = 5
2. OperatingLimiter (ограничение времени операций)
Назначение: контролирует суммарное время выполнения операций в скользящем окне.
OperatingLimiter — конфигурация
interface OperatingLimitConfig {
windowMs: number // Период времени в миллисекундах (по умолчанию: 600000 = 10 минут)
limitMs: number // Максимальное время выполнения в миллисекундах (по умолчанию: 480000 = 480 секунд)
heavyPercent: number // Порог для уведомлений о тяжёлых запросах (%)
}
OperatingLimiter — принцип работы
- Скользящее окно: отслеживает время выполнения за последние 10 минут
- Учёт по методам: статистика ведётся отдельно для каждого метода
- Буфер безопасности: в расчётах используется
limitMs - 5000(буфер 5 секунд) - Блокировка: при достижении лимита блокирует выполнение до сброса статистики
OperatingLimiter — особенности
- Для batch-запросов анализирует все вложенные методы
- Автоматическая очистка устаревших данных (> windowMs + 10 секунд)
- Логирование тяжёлых запросов при превышении
heavyPercent
3. AdaptiveDelayer (адаптивные задержки)
Назначение: динамически рассчитывает задержки на основе опыта выполнения предыдущих запросов.
AdaptiveDelayer — конфигурация
interface AdaptiveConfig {
enabled: boolean // Включить адаптивные задержки
thresholdPercent: number // Порог активации (% от operating limit)
coefficient: number // Множитель задержки (0.01 = 1% от оставшегося времени блокировки)
maxDelay: number // Максимальная задержка в миллисекундах
}
AdaptiveDelayer — алгоритм расчёта задержки
Если operating текущего метода > (limitMs × thresholdPercent / 100):
Если operating_reset_at > текущее время:
Задержка = (operating_reset_at - текущее время) × coefficient
Иначе:
Задержка = 7000 мс (значение по умолчанию)
Итоговая задержка = min(рассчитанная, maxDelay)
AdaptiveDelayer — особенности
- Для batch-запросов выбирает максимальную задержку среди всех методов
- Не блокирует выполнение, только добавляет задержку
- Использует статистику из OperatingLimiter
4. RestrictionManager (главный координатор)
Назначение: управляет всеми типами ограничений и обработкой ошибок.
Порядок применения ограничений
- Проверка Operating Limit: проверяет лимит времени операций
- Адаптивная задержка: при необходимости применяет адаптивную задержку
- Rate Limit: проверяет и применяет ограничение частоты (цикл для параллельных запросов)
Обработка ошибок
// Определение типа ошибки
#isRateLimitError(error): boolean // 503 или QUERY_LIMIT_EXCEEDED
#isOperatingLimitError(error): boolean // 429 или OPERATION_TIME_LIMIT
#isNonRetryableClientError(error): boolean // HTTP 4xx (кроме 429 / 408)
#isNeedThrowError(error): boolean // Критические ошибки (повтор бессмыслен)
Стратегия повторов
- Для ошибок лимитов: экспоненциальная задержка с учётом номера попытки
- Для клиентских ошибок (HTTP 4xx, кроме 429 / 408): без повторов — ответ
4xxдетерминирован, поэтому повтор не изменит исход. Ошибка выходит из цикла повторов на первой попытке; далее применяется обычная классификация hard/soft (выбрасывается какAjaxErrorили возвращается внутриAjaxResultдля soft-кодов).429повторяется как rate/operating limit;408(таймаут запроса) остаётся временным. - Для прочих ошибок: базовый backoff с jitter (±10%)
- Критические ошибки: без повторов
Конфигурации по умолчанию
Параметры по умолчанию для обычных тарифов (standard)
{
rateLimit: {
burstLimit: 50, // 50 одновременных запросов
drainRate: 2, // 2 запроса в секунду
adaptiveEnabled: true
},
operatingLimit: {
windowMs: 600000, // 10 минут
limitMs: 480000, // 480 секунд (8 минут)
heavyPercent: 80 // Уведомление при 80% использования
},
adaptiveConfig: {
enabled: true,
thresholdPercent: 80, // Активация при 80% лимита
coefficient: 0.01, // 1% от оставшегося времени блокировки
maxDelay: 7000 // Максимум 7 секунд задержки
},
maxRetries: 3,
retryDelay: 1000,
retryOnNetworkError: true
}
Параметры для тарифа Enterprise
{
...standard,
rateLimit: {
burstLimit: 250, // 250 одновременных запросов
drainRate: 5, // 5 запросов в секунду
adaptiveEnabled: true
}
}
Параметры для массовой обработки данных
{
...standard,
rateLimit: {
burstLimit: 30,
drainRate: 1,
adaptiveEnabled: true
},
operatingLimit: {
windowMs: 600_000,
limitMs: 480_000,
heavyPercent: 50 // Более высокий порог для уведомлений
},
adaptiveConfig: {
enabled: true,
thresholdPercent: 50, // Больше порог
coefficient: 0.015, // Больше пауза
maxDelay: 10_000 // Макс 10 секунд
},
maxRetries: 5 // Больше попыток
}
Параметры реального времени
{
...standard,
adaptiveConfig: {
enabled: false, // Выключено
thresholdPercent: 100,
coefficient: 0.001,
maxDelay: 480_000
},
maxRetries: 1
}
Мониторинг и статистика
Получение статистики
import { ApiVersion } from '@bitrix24/b24jssdk'
// const $b24 = ...
const statsV2 = $b24.getHttpClient(ApiVersion.v2).getStats()
const statsV3 = $b24.getHttpClient(ApiVersion.v3).getStats()
Структура статистики:
{
// Общая статистика
retries: number, // Число попыток повтора
consecutiveErrors: number, // Последовательные ошибки
limitHits: number, // Попадания в лимит
// Rate Limiter
tokens: number, // Текущее число токенов
burstLimit: number, // Текущий burst limit
drainRate: number, // Текущий drain rate
// Adaptive Delayer
adaptiveDelays: number, // Число применённых задержек
totalAdaptiveDelay: number, // Суммарное время задержек
adaptiveDelayAvg: number, // Средняя задержка
// Operating Limiter
heavyRequestCount: number, // Число тяжёлых запросов
operatingStats: { // Статистика по методам (в секундах)
[method: string]: number
},
// Ошибки по методам
errorCounts: {
[method: string]: number
}
}
Сброс статистики
// Полный сброс всей статистики лимитера
import { ApiVersion } from '@bitrix24/b24jssdk'
// const $b24 = ...
await $b24.getHttpClient(ApiVersion.v2).reset()
await $b24.getHttpClient(ApiVersion.v3).reset()
Долгие запросы и неидемпотентные вызовы
SDK поставляется с 30-секундным таймаутом axios и повторяет неудавшиеся запросы до maxRetries раз. Оба значения по умолчанию неверны для долгих, неидемпотентных REST-методов — канонический пример crm.documentgenerator.document.add, который может рендерить шаблон 10‒60 секунд и является исходным репортом из issue #24.
Почему по умолчанию это ломается
Когда сервер отвечает дольше таймаута axios, разворачивается такая последовательность:
- Клиент открывает запрос, сервер начинает обработку.
- Axios срабатывает по таймауту, SDK видит
REQUEST_TIMEOUT(code: ECONNABORTED). REQUEST_TIMEOUTтрактуется как временная ошибка → SDK повторяет.- Сервер, не зная, что клиент сдался, завершает первый запрос и сохраняет документ. Затем принимает повтор и сохраняет ещё один.
- После
maxRetriesпопыток SDK выбрасывает исходную ошибку (её реальный код), и у вызывающего остаётся 2-3 дубликата в CRM.
Тот же риск касается каждого неидемпотентного метода: crm.deal.add,
crm.contact.add, disk.folder.uploadfile, tasks.task.add, любого кастомного REST-
endpoint, создающего состояние.
Рекомендуемая конфигурация
Используйте этот паттерн для любого вызова, создающего сущность, независимо от ожидаемой длительности:
import { ApiVersion, ParamsFactory } from '@bitrix24/b24jssdk'
// const $b24 = ...
// 1. Поднимите таймаут, чтобы клиент действительно ждал медленную операцию.
const clientAxios = $b24.getHttpClient(ApiVersion.v2).ajaxClient
clientAxios.defaults.timeout = 120_000 // по умолчанию 30_000
// 2. Отключите повторы на транспортных ошибках, чтобы клиентский таймаут никогда
// не создавал дубликаты сущностей на сервере.
await $b24.setRestrictionManagerParams({
...ParamsFactory.getDefault(),
retryOnNetworkError: false
})
// Теперь безопасно для неидемпотентных вызовов.
const result = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.document.add',
params: { templateId: 42, entityTypeId: 2, entityId: 6014, values: {} }
})
retryOnNetworkError: false всё равно создаёт дубликаты
на нестабильной сети (сервер ответил, но ответ не дошёл). Флаг
без таймаута срабатывает слишком рано на запросах, которые завершились бы
за 35-40 секунд.Точечное применение — переопределение на вызов
Если строгое поведение нужно только для конкретного участка кода, создайте отдельный
B24Hook для этого вызова вместо мутации глобального:
import { B24Hook, ParamsFactory } from '@bitrix24/b24jssdk'
import { ApiVersion } from '@bitrix24/b24jssdk'
const $b24Strict = B24Hook.fromWebhookUrl(hookUrl, {
restrictionParams: {
...ParamsFactory.getDefault(),
retryOnNetworkError: false
}
})
$b24Strict.getHttpClient(ApiVersion.v2).ajaxClient.defaults.timeout = 120_000
await $b24Strict.actions.v2.call.make({ method: 'crm.deal.add', params: {/*…*/} })
Крайний случай — отключить все повторы
Если вы не можете позволить ни одного повтора ни при каких обстоятельствах (например, биллинговая
операция), задайте maxRetries: 1:
import { ParamsFactory } from '@bitrix24/b24jssdk'
await $b24.setRestrictionManagerParams({
...ParamsFactory.getDefault(),
maxRetries: 1
})
Это затрагивает каждый класс ошибок, а не только транспортные, поэтому используйте осторожно.
Настройка классификации ошибок
SDK группирует коды ошибок REST на три категории:
- Hard-ошибки — выбрасываются сразу как
AjaxError. Без повтора. Используются для фатальных ситуаций (сбои авторизации, неверные аргументы, удалённые порталы). - Soft-ошибки — возвращаются внутри
AjaxResultкак payload ошибки, не выбрасываются. Используются для кодов, которые вызывающий код обычно проверяет в рамках обычного потока управления (например,ENTITY_NOT_FOUND, ошибки валидации v3). - Всё остальное — возможность повтора определяется HTTP-статусом. Клиентские ошибки
(HTTP
4xx, кроме429и408) никогда не повторяются — они детерминированы, поэтому SDK быстро завершается на первой попытке независимо от того, перечислен ли код ошибки. Временные состояния (5xx,429,408, сетевые ошибки) повторяются доmaxRetriesраз с backoff и jitter.
Встроенные списки покрывают стандартную REST-поверхность Bitrix24. Если ваше приложение
использует кастомные REST-endpoint-ы (например, из локального приложения или обработчика placement), которые
возвращают свои коды ошибок со статусом не 4xx, эти коды будут
повторяться по умолчанию — что неверно для неидемпотентных бизнес-ошибок.
Расширьте классификацию через hardErrorCodes и softErrorCodes:
import { ParamsFactory } from '@bitrix24/b24jssdk'
// const $b24 = ...
await $b24.setRestrictionManagerParams({
...ParamsFactory.getDefault(),
// Эти будут выброшены сразу вместо повтора:
hardErrorCodes: [
'DOCUMENT_GENERATOR_ALREADY_IN_QUEUE', // бизнес-код: не повторять
'MY_APP_INVALID_PAYLOAD' // кастомный REST: нужна правка на стороне вызывающего
],
// Эти будут возвращены в AjaxResult вместо выброса:
softErrorCodes: [
'MY_APP_VALIDATION_FAILED' // ожидается в рамках обычного потока
]
})
INTERNAL_SERVER_ERROR, коды валидации v3 и т.д.) остаются на месте. Это
предотвращает случайное отключение критичных защит, таких как обнаружение
expired_token.Встроенные списки доступны как статические поля для справки:
import { RestrictionManager } from '@bitrix24/b24jssdk'
console.log(RestrictionManager.BUILT_IN_HARD_ERROR_CODES)
console.log(RestrictionManager.BUILT_IN_SOFT_ERROR_CODES)
Глобальная область setRestrictionManagerParams в пределах экземпляра
setRestrictionManagerParams() мутирует весь экземпляр $b24 — он
перенастраивает единственный RestrictionManager, общий для каждого запроса, идущего
через этот экземпляр. Это не переопределение на отдельный вызов.
На сервере это грабли, когда один экземпляр $b24 разделяется между
параллельными запросами (типовой паттерн: синглтон уровня модуля, переиспользуемый многими
HTTP-обработчиками). Изменение параметров для одного запроса меняет их для всех
запросов в полёте на этом экземпляре.
import { ParamsFactory } from '@bitrix24/b24jssdk'
// ⚠️ АНТИ-ПАТТЕРН: общий $b24, параллельные обработчики
// Запрос A — неидемпотентное создание, которое хочет ОТКЛЮЧИТЬ повторы:
await $b24.setRestrictionManagerParams({
...ParamsFactory.getDefault(),
retryOnNetworkError: false
})
await $b24.actions.v2.call.make({
method: 'crm.deal.add',
params: { fields: { TITLE: 'Deal A' } }
})
// Запрос B — выполняется параллельно на ТОМ ЖЕ $b24 — теперь ТОЖЕ видит
// retryOnNetworkError: false, хотя никогда об этом не просил. А если
// Запрос B первым вызовет setRestrictionManagerParams, создание из
// Запроса A незаметно выполнится с параметрами B. Побеждает последний
// записавший — глобально.
Поскольку мутация глобальна и нет изоляции на уровне запроса, два
параллельных потока createDeal могут затирать друг у друга конфигурацию
rate-limit / повторов.
Безопасные паттерны
- Выделенный экземпляр
$b24для неидемпотентных / нестандартных потоков. Постройте отдельный экземпляр (или передайтеrestrictionParamsпри конструировании) для той ветки кода, которой нужно другое поведение, чтобы она никогда не разделяла состояние с общим пулом по умолчанию:import { B24Hook, ParamsFactory, ApiVersion } from '@bitrix24/b24jssdk' const $b24Strict = B24Hook.fromWebhookUrl(hookUrl, { restrictionParams: { ...ParamsFactory.getDefault(), retryOnNetworkError: false } }) $b24Strict.getHttpClient(ApiVersion.v2).ajaxClient.defaults.timeout = 120_000 await $b24Strict.actions.v2.call.make({ method: 'crm.deal.add', params: { fields: { TITLE: 'Deal A' } } }) - Задавайте параметры ограничений один раз при старте, а не на каждый запрос. Считайте их конфигурацией экземпляра, а не переключателем на отдельный вызов.
- Идемпотентность на уровне приложения. Для неидемпотентных созданий, которые должны пережить повтор независимо от конфигурации ограничений, добавьте собственную защиту идемпотентности (ключ дедупликации / уникальное бизнес-поле, проверяемое перед вставкой), чтобы дублирующий вызов был no-op, даже если повтор проскочит.
Рекомендации по использованию
Конфигурация для разных сценариев:
import { ParamsFactory } from '@bitrix24/b24jssdk'
// const $b24 = ...
// Параметры по умолчанию
$b24.setRestrictionManagerParams( ParamsFactory.getDefault() )
// Пакетная обработка
$b24.setRestrictionManagerParams( ParamsFactory.getBatchProcessing() )
// Real-time
$b24.setRestrictionManagerParams( ParamsFactory.getRealtime() )
// По тарифному плану
$b24.setRestrictionManagerParams( ParamsFactory.fromTariffPlan('enterprise') )
// Динамическое изменение конфигурации
$b24.setRestrictionManagerParams({
...ParamsFactory.getDefault(),
rateLimit: {
burstLimit: 30, // Временное снижение при проблемах
drainRate: 1,
adaptiveEnabled: true
}
})