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

Ограничение запросов и повторы

ShopStory не публикует универсальные числовые лимиты для всех клиентов, API-методов и окружений. Согласуйте гарантированную квоту или SLA с ShopStory и зафиксируйте параметры в интеграционном контракте проекта.

Клиент должен обрабатывать HTTP 429, временные 5xx, timeout и сетевые ошибки.

Правила повторов​

  1. Автоматически повторяйте только безопасные идемпотентные чтения.
  2. Учитывайте Retry-After, если сервер его прислал.
  3. Без Retry-After используйте экспоненциальную задержку со случайным разбросом (jitter).
  4. Ограничивайте число попыток и общую продолжительность запросов (deadline).
  5. Не повторяйте ошибки конфигурации, валидации и идентификации.
  6. Не повторяйте изменение корзины автоматически без idempotency-защиты на стороне ритейлера.

Для ShopStory API проверяйте и транспортный HTTP-код, и status внутри JSON-конверта. Бизнес-ошибка внутри HTTP 200 не является временным сетевым сбоем.

Пример для GET​

Пример рассчитан на Node.js 22+. Общий deadline охватывает запросы и задержки между ними; тот же сигнал ограничивает чтение response body. 8000 мс — выбранный тайм-аут клиента в примере, а не квота или SLA ShopStory.

request-with-retry.mjs
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:

Продолжение request-with-retry.mjs
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, токены и другие учётные данные.