Ограничение запросов и повторы
ShopStory не публикует универсальные числовые лимиты для всех клиентов, API-методов и окружений. Согласуйте гарантированную квоту или SLA с ShopStory и зафиксируйте параметры в интеграционном контракте проекта.
Клиент должен обрабатывать HTTP 429, временные 5xx, timeout и сетевые ошибки.
Правила повторов
- Автоматически повторяйте только безопасные идемпотентные чтения.
- Учитывайте
Retry-After, если сервер его прислал. - Без
Retry-Afterиспользуйте экспоненциальную задержку со случайным разбросом (jitter). - Ограничивайте число попыток и общую продолжительность запросов (
deadline). - Не повторяйте ошибки конфигурации, валидации и идентификации.
- Не повторяйте изменение корзины автоматически без idempotency-защиты на стороне ритейлера.
Для ShopStory API проверяйте и транспортный HTTP-код, и status внутри JSON-конверта. Бизнес-ошибка внутри HTTP 200 не является временным сетевым сбоем.
Пример для GET
Пример рассчитан на Node.js 22+. Общий deadline охватывает запросы и задержки между ними; тот же сигнал ограничивает чтение response body. 8000 мс — выбранный тайм-аут клиента в примере, а не квота или SLA ShopStory.
function retryDelay(response, attempt) {
const value = response?.headers.get('Retry-After');
if (value) {
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
const date = Date.parse(value);
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
}
const backoff = Math.min(8000, 500 * 2 ** attempt);
return backoff + Math.floor(Math.random() * 300);
}
function wait(delay, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const onAbort = () => {
clearTimeout(timer);
reject(signal.reason);
};
const timer = setTimeout(() => {
signal?.removeEventListener('abort', onAbort);
resolve();
}, delay);
signal?.addEventListener('abort', onAbort, { once: true });
});
}
async function getWithRetry(url, { attempts = 3, deadlineMs = 8000, signal } = {}) {
if (!Number.isInteger(attempts) || attempts < 1) throw new TypeError('Invalid attempts');
if (!Number.isInteger(deadlineMs) || deadlineMs < 1 || deadlineMs > 2147483647) {
throw new TypeError('Invalid deadlineMs');
}
const deadline = AbortSignal.timeout(deadlineMs);
signal = signal ? AbortSignal.any([signal, deadline]) : deadline;
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
const response = await fetch(url, {
headers: { Accept: 'application/json' },
signal,
});
const retryable = response.status === 429 || response.status >= 500;
if (!retryable) return response;
if (attempt === attempts - 1) return response;
await response.body?.cancel();
await wait(Math.min(retryDelay(response, attempt), deadlineMs), signal);
} catch (error) {
if (signal?.aborted || attempt === attempts - 1) throw error;
await wait(Math.min(retryDelay(null, attempt), deadlineMs), signal);
}
}
}
Добавьте следующий код в тот же файл и запустите его с выданным SHOPSTORY_APPLICATION_ID:
const applicationId = process.env.SHOPSTORY_APPLICATION_ID;
if (!applicationId) throw new Error('Set SHOPSTORY_APPLICATION_ID');
const url = new URL('https://app.shopstory.live/v3/streams');
url.search = new URLSearchParams({ applicationId, limit: '10', offset: '0' });
const response = await getWithRetry(url, { attempts: 3, deadlineMs: 8000 });
if (!response.ok) throw new Error('ShopStory HTTP ' + response.status);
const payload = await response.json();
if (payload.status !== 200) throw new Error('ShopStory envelope ' + payload.status);
if (!Array.isArray(payload.body?.availableStreams)) throw new Error('Invalid streams response');
console.log(payload.body.availableStreams.length === 0
? 'Нет опубликованного контента: проверьте ожидаемый тестовый эфир'
: payload.body.availableStreams.map(stream => ({ id: stream.id, status: stream.status })));
Timeout и отмена прерывают запрос или ожидание; итоговую HTTP-ошибку и JSON-конверт код обрабатывает отдельно. Ошибка envelope не запускает новый retry. Подробные состояния и обязательные поля: Формат ответов и Каталог эфиров. Не используйте устаревший ответ как подтверждение покупки.
Опрос API
- Не запускайте отдельный бесконечный polling для каждой карточки товара.
- Останавливайте запросы, когда экран скрыт или пользователь покинул страницу.
- Увеличивайте интервал после ошибок и восстанавливайте его постепенно.
- Объединяйте одинаковые одновременные запросы.
- Для стандартного плеера используйте SDK: он сам управляет сетевыми запросами.
- Частоту запросов кастомного live UI выбирайте с учётом допустимой задержки и квоты проекта.
Каталог и состояние плеера можно кратковременно кэшировать по правилам раздела Кэширование. При покупке merchant-система проверяет актуальный остаток заново.
Метрики
Отслеживайте:
- долю
429,5xxи timeout; - число попыток на исходный запрос;
- время от первого запроса до завершения всех попыток;
- возраст данных, показанных пользователю;
- число переходов интерфейса в деградированный режим.
Не записывайте в метрики полный URL, cookies, токены и другие учётные данные.