6 мин чтенияИнженерия

Как сократить URL в JavaScript с помощью fetch и Node

Сократите URL в JavaScript одним вызовом fetch, затем добавьте тайм-аут, повторы и идемпотентность - и узнайте, почему браузер не должен хранить ключ.

Marius Voß
DevRel · edge infra
Как сократить URL в JavaScript: вызов Node fetch отправляет URL назначения в API сервиса сокращения и получает короткую ссылку, а браузеру запрещено хранить ключ API

Сокращение URL в JavaScript - это один HTTP-запрос POST. Вы отправляете длинную ссылку в API сервиса сокращения с помощью fetch, передаёте ключ API как Bearer-токен и извлекаете короткую ссылку из ответа JSON. Начиная с Node 18 ничего устанавливать не нужно - fetch является глобальным объектом.

Важно, где выполняется этот код. Ключ API сервиса сокращения даёт возможность расходовать квоту, а клиентский JavaScript общедоступен, поэтому браузер не подходит. Всё ниже выполняется на серверной стороне: в скрипте Node, обработчике маршрута или бессерверной функции.

Это версия на JavaScript для общего руководства по сокращению URL, где вместо этого рассматриваются панель управления и сценарий в браузере. Если вы ещё не выбрали сервис, в обзоре бесплатного API сервиса сокращения URL описаны форма запроса, модель аутентификации и ограничения бесплатного тарифа, которые предполагаются в этой статье. Имена полей принадлежат Elido, но сама схема - отправить назначение и получить короткий URL - подходит большинству современных сервисов сокращения.

Самый быстрый способ: один вызов fetch в Node

Поместите ключ в переменную окружения и отправьте назначение в конечную точку links:

const res = await fetch("https://api.elido.app/v1/links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    destination_url: "https://example.com/spring-sale?utm_source=newsletter",
  }),
  signal: AbortSignal.timeout(10_000),
});

if (!res.ok) throw new Error(`shorten failed: ${res.status}`);

const { short_url } = await res.json();
console.log(short_url); // -> https://s.elido.me/ab12cd

Запустите код командой node --env-file=.env shorten.js, и ключ никогда не попадёт в исходный код.

Строка if (!res.ok) - не украшение. fetch отклоняет промис только при сетевой ошибке или прерывании, поэтому ответ 401 или 500 возвращается как полностью разрешённый промис с ok: false. Пропустите эту проверку, и отозванный ключ будет выглядеть как успешный, пока следующий этап не попытается использовать undefined как ссылку. Все, кто переходит с axios, однажды сталкиваются с этой особенностью.

К AbortSignal.timeout тоже стоит привыкнуть. Без него зависшее соединение блокирует выполнение, пока сокет не закроется сам, а в задании cron это означает, что задание просто зависнет.

Почему браузер не подходит для этой задачи

Вставьте этот фрагмент в компонент React, и сломаются две вещи. Первая - CORS: API с аутентификацией по ключу не отправляет Access-Control-Allow-Origin для произвольных сайтов, поэтому браузер блокирует ответ. Чтение справки MDN по CORS обычно заставляет искать прокси, который уберёт ошибку.

Ошибка - лишь симптом. Настоящая проблема вторая: ключ во фронтенд-коде может прочитать любой, кто откроет DevTools или выполнит поиск в бандле. Сборщики не скрывают его, префиксы NEXT_PUBLIC_ прямо сообщают о его назначении, а публичный ключ для платного API означает, что кто-то другой бесплатно расходует вашу квоту.

Вызов fetch из браузера к API сервиса сокращения URL заблокирован CORS, а ключ API открыт в бандле; правильный путь сравнивает этот вариант с вызовом серверного маршрута, который хранит ключ и пересылает запрос

Рабочая схема помещает в середину ваш маршрут. Браузер отправляет обычный URL на ваш конечный адрес, сервер добавляет ключ, а сам ключ остаётся в переменной окружения, которую клиент не видит:

// app/api/shorten/route.js  (Next.js App Router)
export async function POST(request) {
  const { url } = await request.json();

  const res = await fetch("https://api.elido.app/v1/links", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ destination_url: url }),
  });

  const { short_url } = await res.json();
  return Response.json({ short_url });
}

Проверяйте url перед пересылкой. Открытая конечная точка, которая сокращает всё, что ей отправит любой человек, - это открытое перенаправление с дополнительными шагами, а в материале об открытых перенаправлениях описано, как этим пользуются злоумышленники.

Добавьте тайм-аут, повторы и ключ идемпотентности

Успешный сценарий занимает восемь строк. Задание, выполняющееся без участия человека, должно переживать ответ 429, временную ошибку 5xx и более неприятный случай, когда POST выполняется, но ответ не приходит: код прерывается по тайм-ауту, повторяет запрос, и теперь для одного назначения существуют две короткие ссылки.

Ключ идемпотентности устраняет этот пробел. Создавайте его для каждого URL, используйте повторно при каждом повторе этого URL, и API вернёт исходную ссылку вместо создания второй.

import { randomUUID } from "node:crypto";

const API = "https://api.elido.app/v1/links";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function shorten(destinationUrl, { retries = 3 } = {}) {
  const idempotencyKey = randomUUID(); // one per URL, not per attempt

  for (let attempt = 0; attempt < retries; attempt++) {
    const res = await fetch(API, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({ destination_url: destinationUrl }),
      signal: AbortSignal.timeout(10_000),
    });

    if (res.status === 429) {
      await sleep(Number(res.headers.get("Retry-After") ?? 2) * 1000);
      continue;
    }
    if (res.status >= 500) {
      await sleep(2 ** attempt * 1000); // exponential backoff
      continue;
    }
    if (!res.ok) {
      throw new Error(`shorten failed: ${res.status} ${await res.text()}`);
    }

    return (await res.json()).short_url;
  }

  throw new Error(`shorten failed after ${retries} attempts`);
}

Ответ 401 или 403 никогда не исправится сам, поэтому такой запрос сразу вызывает ошибку, не расходуя три попытки. При ответе 429 ожидание длится ровно столько, сколько указано в заголовке Retry-After. Только для 5xx используется экспоненциальная задержка. В подробном разборе ограничений частоты и идемпотентности объясняется, почему наивный цикл повторов превращает сбой у одного поставщика в две ссылки.

Хотите запускать фрагменты как написано? Создайте ключ на бесплатном тарифе, экспортируйте его как ELIDO_API_KEY, и каждый пример на этой странице заработает без изменений.

Как сокращать URL массово и не попасть под ограничение частоты

Очевидный вариант для массовой обработки - Promise.all(urls.map(shorten)). Все запросы отправляются в один и тот же момент, поэтому список из 500 URL превращается в 500 одновременных соединений, и API отвечает на большинство из них кодом 429. Небольшой пул решает проблему: фиксированное число рабочих процессов забирает задачи из общей очереди, поэтому параллелизм не меняется, как бы ни была длинна очередь.

export async function shortenAll(urls, concurrency = 8) {
  const queue = [...urls];
  const results = new Map();

  const worker = async () => {
    while (queue.length) {
      const url = queue.pop();
      try {
        results.set(url, await shorten(url));
      } catch (err) {
        results.set(url, `ERROR: ${err.message}`); // one bad URL must not sink the batch
      }
    }
  };

  await Promise.all(Array.from({ length: concurrency }, worker));
  return results;
}
Promise.all одновременно запускает каждый запрос сокращения и собирает ошибки ограничения частоты; ограниченный пул рабочих процессов поддерживает фиксированное число выполняющихся запросов и безопасно сокращает URL массово

Если использовать исходный URL как ключ, частичный сбой будет виден и его можно будет повторно запустить, а не потерять молча в результате. Установите concurrency согласно документации тарифа; восемь - безопасная отправная точка, а заголовки ответа подскажут, когда значение нужно уменьшить.

Если вы не хотите поддерживать всё это самостоятельно, в API и SDK логика повторов и пагинации уже встроена, а в кратком руководстве по SDK показан типизированный вариант того же вызова.

Где должен находиться код

Почти все случаи укладываются в четыре варианта. Одноразовый скрипт, сокращающий CSV, отлично работает как обычный Node с --env-file. Форме на вашем сайте нужен обработчик маршрута выше: ключ остаётся на сервере, а браузер обращается только к вам. Для достаточно большой пакетной обработки подойдёт рабочий процесс очереди, чтобы ни один HTTP-запрос не ждал её завершения. А этап сборки, который создаёт ссылки кампаний, должен выполняться в CI с ключом в секрете репозитория.

Для сред edge есть одна оговорка: node:crypto там недоступен, но crypto.randomUUID() является глобальным объектом Web Crypto в Vercel Edge, Cloudflare Workers и Deno, поэтому замените импорт, а остальной код останется без изменений.

Место меняется, но список проверок остаётся прежним: ключ в окружении, тайм-аут для каждого запроса, чтение res.ok перед телом и ключ идемпотентности для всего, что может запуститься дважды. Команды, работающие в большом масштабе, обычно переходят к изучению того, что ещё предоставляет API, - кликов, меток и срока действия, - и настраивают вебхуки для событий ссылок, а не опрашивают API.

Читайте опорную серию статей

Эта статья входит в кластер материалов для разработчиков. Начните с руководства по бесплатному API сервиса сокращения URL, чтобы разобраться с формой конечной точки и аутентификацией, а затем перейдите к статье об ограничениях частоты и идемпотентности, где объясняется корректная работа под нагрузкой. Актуальная справка доступна в документации API, а в статье Сервисы сокращения URL для разработчиков рассказывается, на что обратить внимание в API перед выбором сервиса.

Ещё по теме в блоге

Частые вопросы

Как сократить URL в JavaScript?

Отправьте HTTP-запрос POST к API сервиса сокращения URL с помощью fetch, передав длинный URL в теле JSON, а ключ API - как Bearer-токен, затем извлеките короткую ссылку из разобранного ответа. В Node 18 и более новых версиях fetch встроен, поэтому устанавливать пакет не нужно: сформируйте заголовки, отправьте URL назначения в конечную точку links и используйте поле short_url из полученного JSON.

Можно ли сократить URL в браузере с помощью JavaScript?

Не с ключом API в самой странице. Всё, что находится в клиентском JavaScript, можно прочитать в DevTools, поэтому ключ, отправленный в браузер, становится публичным и любой может потратить вашу квоту. Вызывайте сервис сокращения из серверного маршрута или бессерверной функции, а браузер пусть обращается к этому маршруту.

Почему мой запрос fetch к сервису сокращения URL возвращает ошибку CORS?

Потому что API сервиса сокращения не отправляет заголовок Access-Control-Allow-Origin для вашего сайта, и для конечной точки с аутентификацией по ключу это сделано намеренно. Браузер блокирует ответ, даже если запрос мог успешно выполниться. Перенос вызова на серверную сторону убирает браузер из цепочки, а вместе с ним и эту ошибку.

Нужен ли пакет npm, чтобы сокращать URL в Node.js?

Нет. Начиная с версии 18 в Node есть глобальный fetch, а вызов API сервиса сокращения - это один запрос POST с двумя заголовками, поэтому зависимость почти ничего не даёт. Используйте SDK, если вам нужны типизированные ответы, встроенные повторы и помощники для пагинации по множеству конечных точек, а не один вызов.

Как сократить много URL одновременно в Node.js?

Запустите небольшой пул рабочих процессов вместо Promise.all для всего списка, чтобы одновременно выполнялось лишь несколько запросов и вы не превышали ограничение частоты. Отправляйте отдельный Idempotency-Key для каждого URL, чтобы повторный запрос вернул исходную ссылку, а не создал дубликат.

Выбрасывает ли fetch ошибку при ответе 401 или 500?

Нет, и это часто сбивает с толку тех, кто переходит с axios. fetch отклоняет промис только при сетевой ошибке или прерванном запросе, поэтому ответ 401 или 500 приходит как успешно разрешённый, но с ok, равным false. Самостоятельно проверяйте response.ok перед чтением тела, иначе неудачный вызов будет выглядеть успешным.

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Бесплатно, без регистрации · 2 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
how to shorten a url in javascript
url shortener node js
shorten url javascript fetch
javascript url shortener api
bulk shorten urls node
node fetch post json

Читать дальше