Перейти к основному содержимому

Rate limiting и управление трафиком

Текущие лимиты

ЭндпоинтЛимитПериодКлюч
/v3/streams20 запросов1 минутаapplicationId
/v3/translation/quick-state120 запросов1 минутаIP-адрес
/v2/mini-player/stream60 запросов1 минутаapplicationId
/v2/mini-player/online30 запросов1 минутаapplicationId

При превышении возвращается HTTP 429 с заголовком Retry-After в секундах.

Рекомендации

  • Экспоненциальный бэкофф: увеличивайте паузу минимум в 2 раза при повторе.
  • Кэширование: данные каталога можно кэшировать на 30–60 секунд без риска устаревания.
  • Пулы ключей: если у вас несколько приложений, разводите трафик по applicationId.
  • Наблюдайте лимиты: логируйте Retry-After, чтобы понимать реальную нагрузку.

🔄 Примеры реализации Retry Policy

JavaScript: Продвинутый retry с экспоненциальным backoff

Автоматические повторы с учетом Retry-After
class ShopStoryAPIClient {
constructor(applicationId, options = {}) {
this.applicationId = applicationId;
this.baseUrl = 'https://app.shopstory.live';
this.maxRetries = options.maxRetries || 3;
this.initialDelay = options.initialDelay || 1000; // 1 секунда
this.maxDelay = options.maxDelay || 60000; // 60 секунд
}

/**
* Выполнить запрос с автоматическими повторами
*/
async fetchWithRetry(endpoint, params = {}, attempt = 0) {
const url = new URL(`${this.baseUrl}${endpoint}`);
url.searchParams.set('applicationId', this.applicationId);

// Добавляем параметры запроса
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
});

try {
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': 'ShopStoryClient/1.0'
},
signal: AbortSignal.timeout(10000) // 10 секунд таймаут
});

// Успешный ответ
if (response.ok) {
const data = await response.json();
return data;
}

// 429 - Rate Limit
if (response.status === 429) {
if (attempt >= this.maxRetries) {
throw new Error(`Rate limit exceeded after ${this.maxRetries} retries`);
}

// Получаем Retry-After из заголовка
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
const delay = Math.min(retryAfter * 1000, this.maxDelay);

console.warn(
`[Retry ${attempt + 1}/${this.maxRetries}] ` +
`Rate limited. Waiting ${delay}ms (Retry-After: ${retryAfter}s)`
);

await this.sleep(delay);
return this.fetchWithRetry(endpoint, params, attempt + 1);
}

// 5xx - Серверная ошибка
if (response.status >= 500) {
if (attempt >= this.maxRetries) {
throw new Error(`Server error ${response.status} after ${this.maxRetries} retries`);
}

// Экспоненциальный backoff: 1s, 2s, 4s, 8s...
const backoffDelay = Math.min(
this.initialDelay * Math.pow(2, attempt),
this.maxDelay
);

console.warn(
`[Retry ${attempt + 1}/${this.maxRetries}] ` +
`Server error ${response.status}. Waiting ${backoffDelay}ms`
);

await this.sleep(backoffDelay);
return this.fetchWithRetry(endpoint, params, attempt + 1);
}

// 4xx - Клиентская ошибка (не повторяем)
const errorData = await response.json();
throw new Error(
`Client error ${response.status}: ${errorData.body?.error?.message || 'Unknown error'}`
);

} catch (error) {
// Сетевая ошибка или таймаут
if (error.name === 'AbortError' || error.name === 'TypeError') {
if (attempt >= this.maxRetries) {
throw new Error(`Network error after ${this.maxRetries} retries: ${error.message}`);
}

const backoffDelay = Math.min(
this.initialDelay * Math.pow(2, attempt),
this.maxDelay
);

console.warn(
`[Retry ${attempt + 1}/${this.maxRetries}] ` +
`Network error. Waiting ${backoffDelay}ms`
);

await this.sleep(backoffDelay);
return this.fetchWithRetry(endpoint, params, attempt + 1);
}

throw error;
}
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Удобные методы для эндпоинтов
async getStreams(options = {}) {
return this.fetchWithRetry('/v3/streams', {
limit: options.limit || 10,
offset: options.offset || 0,
feedProductId: options.feedProductId,
categoryId: options.categoryId
});
}

async getQuickState(translationId) {
return this.fetchWithRetry('/v3/translation/quick-state', {
translationId
});
}
}

// Использование
const client = new ShopStoryAPIClient('your-app-id', {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 60000
});

try {
const data = await client.getStreams({ limit: 20 });
console.log('Получено стримов:', data.body.availableStreams.length);
} catch (error) {
console.error('Ошибка API:', error.message);
}

Python: Продвинутый retry с декоратором

retry_decorator.py
import time
import requests
from functools import wraps
from typing import Optional, Callable

class RateLimitError(Exception):
"""Ошибка превышения rate limit"""
def __init__(self, retry_after: int):
self.retry_after = retry_after
super().__init__(f"Rate limit exceeded. Retry after {retry_after}s")

class ShopStoryAPIError(Exception):
"""Базовая ошибка API"""
pass

def retry_with_backoff(
max_retries: int = 3,
initial_delay: float = 1.0,
max_delay: float = 60.0,
backoff_factor: float = 2.0
):
"""
Декоратор для автоматических повторов с экспоненциальным backoff
"""
def decorator(func: Callable):
@wraps(func)
def wrapper(*args, **kwargs):
delay = initial_delay

for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)

except RateLimitError as e:
if attempt >= max_retries:
raise ShopStoryAPIError(
f"Rate limit exceeded after {max_retries} retries"
) from e

# Используем Retry-After из ответа
wait_time = min(e.retry_after, max_delay)
print(
f"[Retry {attempt + 1}/{max_retries}] "
f"Rate limited. Waiting {wait_time}s (Retry-After: {e.retry_after}s)"
)
time.sleep(wait_time)

except (requests.exceptions.RequestException, ShopStoryAPIError) as e:
if attempt >= max_retries:
raise ShopStoryAPIError(
f"Request failed after {max_retries} retries: {str(e)}"
) from e

# Экспоненциальный backoff
wait_time = min(delay, max_delay)
print(
f"[Retry {attempt + 1}/{max_retries}] "
f"Error: {str(e)}. Waiting {wait_time}s"
)
time.sleep(wait_time)
delay *= backoff_factor

raise ShopStoryAPIError(f"Failed after {max_retries} retries")

return wrapper
return decorator

class ShopStoryClient:
def __init__(self, application_id: str, timeout: int = 10):
self.application_id = application_id
self.base_url = "https://app.shopstory.live"
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({
'Accept': 'application/json',
'User-Agent': 'ShopStoryClient/1.0'
})

@retry_with_backoff(max_retries=3, initial_delay=1.0, max_delay=60.0)
def _make_request(self, endpoint: str, params: dict) -> dict:
"""Выполнить HTTP запрос с обработкой ошибок"""
params['applicationId'] = self.application_id

response = self.session.get(
f"{self.base_url}{endpoint}",
params=params,
timeout=self.timeout
)

# Проверяем rate limit
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
raise RateLimitError(retry_after)

# Проверяем серверные ошибки
if response.status_code >= 500:
raise ShopStoryAPIError(
f"Server error: {response.status_code}"
)

# Проверяем клиентские ошибки
if response.status_code >= 400:
try:
error_data = response.json()
error_msg = error_data.get('body', {}).get('error', {}).get('message', 'Unknown error')
except:
error_msg = f"HTTP {response.status_code}"

raise ShopStoryAPIError(f"Client error: {error_msg}")

return response.json()

def get_streams(
self,
limit: int = 10,
offset: int = 0,
feed_product_id: Optional[str] = None,
category_id: Optional[int] = None
) -> dict:
"""Получить список стримов с автоматическими повторами"""
params = {
'limit': min(max(1, limit), 100), # Валидация: 1-100
'offset': max(0, offset) # Валидация: >= 0
}

if feed_product_id:
params['feedProductId'] = feed_product_id
if category_id:
params['categoryId'] = category_id

return self._make_request('/v3/streams', params)

def get_quick_state(self, translation_id: str) -> dict:
"""Получить быстрый статус трансляции"""
return self._make_request('/v3/translation/quick-state', {
'translationId': translation_id
})

# Использование
if __name__ == "__main__":
import os

client = ShopStoryClient(
application_id=os.getenv("SHOPSTORY_APP_ID"),
timeout=10
)

try:
# Автоматические повторы при rate limit или сетевых ошибках
streams = client.get_streams(limit=20, offset=0)

print(f"✅ Получено стримов: {len(streams['body']['availableStreams'])}")

for stream in streams['body']['availableStreams'][:5]:
print(f" 📺 {stream['name']}")

except ShopStoryAPIError as e:
print(f"❌ Ошибка API: {e}")

Продвинутый пример: Rate Limit с очередью запросов

Очередь запросов с соблюдением rate limits
class RateLimitedQueue {
constructor(requestsPerMinute = 20) {
this.requestsPerMinute = requestsPerMinute;
this.queue = [];
this.processing = false;
this.requestTimestamps = [];
}

/**
* Добавить запрос в очередь
*/
async enqueue(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.processQueue();
});
}

/**
* Обработать очередь запросов
*/
async processQueue() {
if (this.processing || this.queue.length === 0) {
return;
}

this.processing = true;

while (this.queue.length > 0) {
// Проверяем, не превышен ли лимит
const now = Date.now();
const oneMinuteAgo = now - 60000;

// Удаляем старые timestamp
this.requestTimestamps = this.requestTimestamps.filter(
ts => ts > oneMinuteAgo
);

// Если превысили лимит - ждём
if (this.requestTimestamps.length >= this.requestsPerMinute) {
const oldestRequest = Math.min(...this.requestTimestamps);
const waitTime = 60000 - (now - oldestRequest) + 100; // +100ms запас

console.log(
`⏳ Rate limit: ждём ${Math.ceil(waitTime / 1000)}s ` +
`(${this.requestTimestamps.length}/${this.requestsPerMinute} запросов)`
);

await this.sleep(waitTime);
continue;
}

// Выполняем следующий запрос
const { requestFn, resolve, reject } = this.queue.shift();

try {
this.requestTimestamps.push(Date.now());
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}

// Небольшая пауза между запросами
await this.sleep(100);
}

this.processing = false;
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

// Использование
const queue = new RateLimitedQueue(20); // 20 запросов в минуту
const APP_ID = 'your-app-id';

async function fetchStreamsWithQueue(limit, offset) {
return queue.enqueue(async () => {
const url = `https://app.shopstory.live/v3/streams?applicationId=${APP_ID}&limit=${limit}&offset=${offset}`;
const response = await fetch(url);
return response.json();
});
}

// Пример: загрузить 100 стримов постранично (5 страниц по 20)
async function loadAllStreams() {
const results = [];

for (let page = 0; page < 5; page++) {
const offset = page * 20;
console.log(`📥 Загружаем страницу ${page + 1}/5 (offset: ${offset})`);

const data = await fetchStreamsWithQueue(20, offset);
results.push(...data.body.availableStreams);
}

console.log(`✅ Загружено ${results.length} стримов без превышения rate limit`);
return results;
}

loadAllStreams();

📊 Best Practices по Rate Limiting

✅ Правильный подход

// 1. Кэшируйте результаты
const cache = new Map();
const CACHE_TTL = 60000; // 60 секунд

async function getCachedStreams(appId) {
const cacheKey = `streams_${appId}`;
const cached = cache.get(cacheKey);

if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
console.log('✅ Используем кэш (экономим запрос)');
return cached.data;
}

const data = await fetchStreams(appId);
cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
}

// 2. Батчинг запросов
async function loadMultipleProducts(productIds) {
// ❌ Плохо: 100 запросов
// for (const id of productIds) {
// await fetchProduct(id);
// }

// ✅ Хорошо: группируем в один запрос
const streams = await fetchStreams({
feedProductId: productIds.join(',') // Если API поддерживает
});
}

// 3. Экспоненциальный backoff всегда
async function fetchWithBackoff(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === retries - 1) throw error;
await sleep(Math.pow(2, i) * 1000);
}
}
}

❌ Антипаттерны

// ❌ 1. Бесконечные retry без задержки
while (true) {
try {
await fetch(url); // Быстро исчерпает лимиты!
break;
} catch {}
}

// ❌ 2. Игнорирование Retry-After
if (response.status === 429) {
await sleep(1000); // Игнорируем Retry-After заголовок!
retry();
}

// ❌ 3. Синхронная бомбардировка
Promise.all([
fetch(url1),
fetch(url2),
// ... 100 запросов одновременно
fetch(url100)
]); // Гарантированно превысит лимит!

Мониторинг

  • Подключите уведомления из /status.
  • Настройте алерты на рост доли 429 в логах CDN/bff.
  • Делитесь метриками с командой ShopStory — мы можем пересмотреть лимиты под ваши сценарии.

Перейдите к кешированию, чтобы ускорить ответы и снизить нагрузку.