Три 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.
Коли вам справді потрібно створити тисячі посилань, не зациклюйтесь на 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 вище зі створеннями: повторна спроба і ключ ідемпотентності - дві половини одної гарантії.
Два обмеження, які варто пам'ятати. Ключ прив'язаний до 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 на день