Elido
7 хв читанняІнженерія

API скорочувача URL: ліміти запитів, повторні спроби, ідемпотентність

Як викликати API скорочувача URL у продакшні: token-bucket ліміти, коди статусу для повторних спроб з backoff, та ключі ідемпотентності, що запобігають дублікатам.

Marius Voß
DevRel · edge infra
Token bucket, що тарифікує API-запити; цикл повторних спроб з backoff; ключ ідемпотентності, що дедуплікує повторний виклик створення - у кольоровій палітрі Elido

Три endpoint'и, заголовок авторизації, JSON-тіло. API скорочувача URL - одна з найпростіших інтеграцій у будь-якому беклозі, і quickstart дає вам робоче коротке посилання за кілька хвилин. Що quickstart пропускає - це все, що відбувається, коли інтеграція працює під навантаженням: rate limiter відкидає запити, транзитний 503 посеред пакетної операції, черга задач доставляє одне й те саме повідомлення двічі. Помилитесь тут - отримаєте дублікати посилань, втрачену роботу і шторм 429-помилок, що лише погіршує ситуацію.

Цей пост - компаньйон з налаштування продакшн-інтеграції до API quickstart. Він охоплює три механіки, що відрізняють демо від надійної інтеграції: ліміти запитів і як регулювати темп відносно них, які помилки повторювати і як відступати з backoff, та ключі ідемпотентності, що не дають повторній спробі створити друге посилання. Приклади використовують API Elido, але патерни однакові для будь-якого добре побудованого API скорочувача посилань. Якщо ви розглядаєте короткі посилання як інфраструктуру, якою управляєте з коду, ширше обгрунтування цього є в короткі посилання як Terraform.

Ліміти запитів: token bucket і три заголовки#

Elido тарифікує API за допомогою token bucket, прив'язаного до workspace. Опубліковані стійкі швидкості: 10 запитів на секунду на Free, 100 на Pro, 500 на Business та узгоджена стеля на Enterprise. Pro має burst-ємність 200 - тобто повний bucket дозволяє надіслати 200 запитів одразу, перш ніж швидкість повернеться до стійких 100 на секунду. Більшість задач зі створення посилань вписуються в burst і взагалі не відчувають ліміту.

Здогадуватись не потрібно. Кожна відповідь містить три заголовки:

  • X-RateLimit-Limit - поточна стеля запитів на секунду.
  • X-RateLimit-Remaining - токени, що залишились у поточному вікні.
  • X-RateLimit-Reset - Unix timestamp, коли bucket поповниться.

Добре налаштований клієнт читає X-RateLimit-Remaining і сповільнюється до того, як досягне нуля, замість того щоб влітати в стіну 429-помилок і реагувати після факту. Проактивне регулювання темпу підтримує рівну пропускну здатність; реактивна повторна спроба після кожного відхилення витрачає round trip'и і, якщо всі клієнти повторюють одночасно, породжує thundering herd.

Token bucket поповнюється зі швидкістю workspace, поки запити витрачають токени; показані три заголовки rate-limit у відповіді; 429 повертається, коли bucket порожній

Коли вам справді потрібно створити тисячі посилань, не зациклюйтесь на endpoint'і одиночного створення. POST /v1/links/bulk приймає до 1000 посилань за один запит і рахується як одна одиниця відносно ліміту. Один bulk-виклик переміщує тисячу посилань за ціну одного токена; тисяча одиночних викликів спалює тисячу токенів і більшу частину вашого burst. Bulk-шлях - це те, як імпорт з Google Sheets переміщує посилання цілої кампанії без спрацьовування limiter'а.

429 Too Many Requests - статус, який RFC 6585 резервує саме для цього - повертається зі значенням retry_after, що вказує, скільки секунд чекати. Поважайте його. Це число - limiter точно повідомляє вам, коли буде доступний токен, і це краща інформація, ніж будь-яке припущення вашого backoff.

Повторні спроби: які коди і як відступати з backoff#

Не кожна помилка варта повторної спроби, а повторення неправильної - це шлях перетворити малий збій на аварію. Розподіліть відповіді на два кошики.

Повторюйте ці, бо вони транзитні: 429 (занадто швидко), і 500, 502, 503, 504 (серверна або gateway-помилка, яка може зникнути сама). Не повторюйте ці, бо той самий запит провалиться ідентично: 400 (некоректне payload), 401 (токен відсутній або неправильний), 403 (токен не має потрібного scope), 404 (ресурс не існує або не ваш), і 409 (конфлікт slug або редагування з застарілою версією). Перший кошик - «зачекайте і спробуйте знову». Другий - «виправте код або вхідні дані». Повторення 400 у щільному циклі просто перетворює баг у DoS-атаку на самого себе.

Для кодів, що підлягають повторній спробі, алгоритм, який має значення, - exponential backoff з jitter. Простий exponential backoff - подвоювати час очікування з кожною спробою - все одно синхронізує клієнтів, бо кожен клієнт, що впав в один момент, також повторює в одні й ті ж моменти. Додавання випадковості розосереджує їх. Публікація AWS про exponential backoff and jitter є канонічним посиланням і показує, чому версія з jitter різко скорочує конкуренцію. Компактний варіант на TypeScript:

const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function withRetry<T>(
  call: () => Promise<Response>,
  max = 5,
): Promise<Response> {
  let attempt = 0;
  while (true) {
    const res = await call();
    if (res.ok || !RETRYABLE.has(res.status) || attempt >= max) return res;

    // Honor server guidance first; otherwise back off exponentially with full jitter.
    const retryAfter = Number(res.headers.get("retry-after"));
    const base =
      Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : Math.min(1000 * 2 ** attempt, 20_000);
    const wait = Math.random() * base; // full jitter
    await new Promise((r) => setTimeout(r, wait));
    attempt++;
  }
}

Три речі роблять це безпечним, а не небезпечним. Обмежує кількість спроб, тому стійкий збій виразно провалюється замість нескінченного кручення. Поважає Retry-After, коли сервер його надсилає, і лише тоді відступає до обчисленого backoff. І джитерує, щоб флот воркерів, що відновлюється від одного збою, не кинувся одночасно. Офіційні SDK реалізують ту саму політику з коробки - @elido/sdk, elido-python і Go-клієнт повторюють рівно п'ять транзитних кодів з jittered backoff - і це головна причина обирати SDK замість саморобного HTTP-клієнта.

Є одне правило, що пов'язує повторні спроби з наступним розділом: повторна спроба create безпечна лише тоді, коли create є ідемпотентним. Інакше кожна спроба ризикує породити друге посилання.

Ідемпотентність: як не створювати дублікати посилань#

Класична помилка виглядає так. Ваш воркер створює коротке посилання, посилання створюється, але 200 не повертається назад - з'єднання обривається на зворотному шляху. Воркер бачить timeout, вважає, що сталася помилка, і повторює. Тепер у вас два посилання для однієї кампанії. При масштабуванні дашборд заповнюється /foo, /foo-1, /foo-2, і дублікати спотворюють усі подальші звіти.

Ключі ідемпотентності закривають цей розрив. Надішліть заголовок Idempotency-Key у запиті, що змінює стан - будь-який рядок до 255 символів - і сервер зберігає відповідь, прив'язану до нього. Надішліть той самий ключ знову - отримаєте початкову відповідь назад, із тим самим статусним кодом і тілом, без повторного виконання операції. Патерн той самий, що Stripe документує для ідемпотентних запитів, і це стандартний спосіб зробити ненадійну мережу безпечною для записів.

Деталь, що вирішує все, - звідки береться ключ. Не генеруйте випадковий ключ для кожної спроби - це знищує сенс, бо тоді кожна повторна спроба виглядає як нова операція. Виводьте його зі стабільного бізнес-ідентифікатора, щоб одна й та сама логічна дія завжди давала однаковий ключ:

const link = await elido.links.create(
  { destinationUrl: order.landingUrl },
  { idempotencyKey: `order-${order.id}-link` },
);

Тепер повторна спроба тієї ж задачі знову несе order-12345-link, потрапляє на збережену відповідь і повертає вже існуюче посилання. Рівно одне посилання на замовлення, скільки б разів черга не передоставила. Саме це дозволяє безпечно поєднувати цикл backoff вище зі створеннями: повторна спроба і ключ ідемпотентності - дві половини одної гарантії.

Черга задач at-least-once надсилає два запити create з однаковим ключем ідемпотентності; сервер зберігає першу відповідь і повертає її на другий запит, тому створюється рівно одне посилання

Два обмеження, які варто пам'ятати. Ключ прив'язаний до workspace: той самий ключ у двох workspace'ах створить два посилання - це правильно для multi-tenant API, але дивує команди, що вважають ключі глобальними. І кеш не є вічним - у Elido він зберігається 24 години, прив'язаний до (workspace, key). Повторна спроба в межах вікна дедуплікує; повторна спроба через три дні, від задачі, яка нарешті відновилась, створить нове посилання. Для багатоденних пакетних задач не покладайтесь лише на ключ. Зберігайте ID посилання, повернений при першому успіху, і перевіряйте його перед повторним викликом create. IETF стандартизує цей заголовок у чернетці Idempotency-Key, і застереження про вікно в 24 години також там зазначене.

Якщо ви налаштовуєте API-інтеграцію сьогодні і хочете, щоб вона витримала власні повторні спроби, починайте з безкоштовного workspace, згенеруйте service-account токен і поставте ключ ідемпотентності в самий перший виклик create - а не додавайте його після того, як з'являться дублікати.

Збираємо все разом#

Продакшн-якісний виклик create - це три механіки, складені докупи. Регулюйте темп відносно rate-limit заголовків, щоб рідко отримувати 429. Огортайте виклик у jittered backoff, що повторює лише транзитні коди і поважає Retry-After. Несіть ключ ідемпотентності, виведений з бізнес-ID, щоб повторна спроба була безпечною. З офіційним SDK перші два йдуть безкоштовно, а ви надаєте лише ключ:

import { Elido, ElidoRateLimitError } from "@elido/sdk";

const elido = new Elido({ token: process.env.ELIDO_TOKEN! });

export async function shortenForOrder(order: Order) {
  try {
    return await elido.links.create(
      { destinationUrl: order.landingUrl, tags: [`order:${order.id}`] },
      { idempotencyKey: `order-${order.id}-link` },
    );
  } catch (err) {
    if (err instanceof ElidoRateLimitError) {
      // SDK already retried with backoff; we are still limited. Defer the job.
      throw new RetryableJobError(err.retryAfter);
    }
    throw err; // non-retryable: surface it
  }
}

Нічого екзотичного. Це та сама дисципліна, якої заслуговує будь-який API зі значним числом записів, застосована до посилань. Нагорода - інтеграція, що робить правильні речі під навантаженням, замість того щоб тихо псувати ваш інвентар посилань. Щодо read-сторони того ж API - отримання даних кліків без перевантаження limiter'а - компроміси описані в webhooks vs polling для відстеження кліків, а повна поверхня endpoint'ів є на сторінці API and SDKs і в огляді developer solutions.

Пов'язані статті в блозі#

Спробуйте Elido

Вставте URL - отримайте коротке посилання

Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.

Безкоштовно, без реєстрації · 2 на день

Спробуйте Elido

URL-скорочувач із хостингом у ЄС: власні домени, глибока аналітика, відкритий API. Безкоштовний тариф - без кредитної картки.

Теги
url shortener api rate limits
api idempotency key
retry with exponential backoff
429 too many requests
link shortener api
idempotent requests

Читати далі