Сокращение URL в Ruby - это один POST-запрос. Отправьте длинную ссылку в API сокращателя с помощью Net::HTTP, передайте ключ API как Bearer-токен и извлеките short_url из полученного JSON. Гем не нужен, что важно, когда скрипт должен работать на машине, которой вы не управляете.
Формат эндпоинта и модель аутентификации ниже описаны в обзоре бесплатного API сокращателя URL; эта статья посвящена Ruby и является специализированной версией общего руководства по сокращению URL, в котором рассматривается маршрут через панель управления.
Та же серия для других сред выполнения: Python, JavaScript, PHP и Go. Имена полей принадлежат Elido, а этот формат подходит большинству современных сокращателей.
Самый быстрый способ: Net::HTTP.post
Два require, один вызов:
require "json"
require "net/http"
response = Net::HTTP.post(
URI("https://api.elido.app/v1/links"),
{ destination_url: "https://example.com/spring-sale?utm_source=newsletter" }.to_json,
"Authorization" => "Bearer #{ENV.fetch('ELIDO_API_KEY')}",
"Content-Type" => "application/json",
)
raise "shorten failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
puts JSON.parse(response.body)["short_url"] # => https://s.elido.me/ab12cd
Использование ENV.fetch, а не ENV[], намеренно: при отсутствии ключа выполнение сразу прервётся, вместо того чтобы отправить Bearer и получить непонятный ответ 401.
Строка unless response.is_a?(Net::HTTPSuccess) - та, которую многие пропускают. Net::HTTP выдаёт исключение при недоступном сокете или тайм-ауте, но больше ни при каких обстоятельствах, поэтому ответ 401 с телом ошибки выглядит как успешный, пока JSON.parse не вернёт хеш без short_url.
Добавьте тайм-ауты, повторные попытки и ключ идемпотентности
У Net::HTTP.post есть недостаток, который нельзя исправить: он не принимает аргументы тайм-аутов. Для любого неинтерактивного сценария перейдите на Net::HTTP.start, который их поддерживает.
Заодно устраните проблему, которую наивная повторная попытка только усугубляет. Если POST достигает API, а ответ теряется по пути обратно, код ждёт истечения тайм-аута, повторяет запрос и создаёт вторую ссылку для того же назначения. Стабильный Idempotency-Key закрывает эту лазейку: хешируйте целевой адрес, и API вернёт исходную ссылку вместо создания новой.
require "digest"
def shorten(destination, attempts: 3)
uri = URI("https://api.elido.app/v1/links")
key = Digest::SHA256.hexdigest(destination) # stable across retries and re-runs
attempts.times do |attempt|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
open_timeout: 5, read_timeout: 10) do |http|
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('ELIDO_API_KEY')}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = key
request.body = { destination_url: destination }.to_json
http.request(request)
end
case response
when Net::HTTPSuccess then return JSON.parse(response.body)["short_url"]
when Net::HTTPTooManyRequests then sleep(response["Retry-After"].to_i.clamp(1, 60))
when Net::HTTPServerError then sleep(2**attempt) # 1s, 2s, 4s
else raise "shorten failed: #{response.code} #{response.body}"
end
end
raise "shorten failed after #{attempts} attempts"
end
Конструкция case по классу ответа читается лучше, чем набор сравнений кодов статуса, и явно показывает политику: при 429 нужно ждать столько, сколько просит сервер, при 5xx применяется экспоненциальная задержка, а любой другой ответ, например 401 или 422, выдаёт исключение с первой попытки, поскольку повтор не поможет. В подробном разборе лимитов и идемпотентности полностью описана семантика заголовков.
Хотите запустить этот код без изменений? Создайте ключ на бесплатном тарифе, экспортируйте его как ELIDO_API_KEY, и каждый фрагмент кода на этой странице будет работать без изменений.
Вариант с Faraday для уже существующего приложения
В приложении Rails самописный цикл имеет неподходящую структуру. Faraday предоставляет один объект соединения с прикреплённым заголовком аутентификации, JSON в обоих направлениях и объявленную, а не написанную вручную политику повторных попыток:
# Gemfile: gem "faraday" and gem "faraday-retry"
ELIDO = Faraday.new(url: "https://api.elido.app") do |f|
f.request :json
f.request :retry, max: 3, interval: 0.5, backoff_factor: 2,
retry_statuses: [429, 500, 502, 503, 504]
f.response :json
f.response :raise_error
f.headers["Authorization"] = "Bearer #{ENV.fetch('ELIDO_API_KEY')}"
f.options.timeout = 10
end
def shorten(destination)
ELIDO.post("/v1/links",
{ destination_url: destination },
{ "Idempotency-Key" => Digest::SHA256.hexdigest(destination) })
.body["short_url"]
end
Перед вставкой этого кода нужно знать две вещи. В Faraday 2 middleware для повторных попыток вынесен в отдельный гем faraday-retry, поэтому в Gemfile нужна отдельная строка. Кроме того, raise_error меняет поведение Net::HTTP на противоположное: теперь ответы 4xx и 5xx вызывают Faraday::ClientError и Faraday::ServerError, что и нужно задаче, которая должна повторяться, а не сохранять nil.
Сократите список, не нарушив лимит запросов
Ruby освобождает глобальную блокировку, пока поток ждёт ввода-вывода, поэтому здесь потоки действительно полезны даже в CRuby. Но нельзя создавать по одному потоку на каждый URL: CSV-файл из 2 000 строк превратится в 2 000 сокетов и стену ответов 429. Queue в сочетании с фиксированным пулом сохраняет постоянную степень параллелизма независимо от длины списка.
def shorten_all(urls, concurrency: 8)
queue = Queue.new
results = {}
mutex = Mutex.new
urls.each { |u| queue << u }
concurrency.times { queue << :done }
workers = concurrency.times.map do
Thread.new do
while (url = queue.pop) != :done
value = begin
shorten(url)
rescue => e
"ERROR: #{e.message}" # one bad row must not sink the batch
end
mutex.synchronize { results[url] = value }
end
end
end
workers.each(&:join)
results
end
Ключом результата служит исходный URL, поэтому частичный сбой остаётся видимым, а партию можно запустить повторно. Mutex обязателен: обычный Hash, в который одновременно записывают восемь потоков, создаёт гонку данных, и она проявится именно при важном запуске.
В Rails оберните shorten в Active Job и передайте управление задержкой адаптеру очереди. Повторная попытка внутри действия контроллера удерживает поток Puma во время каждого sleep, а ограниченный лимитом импорт может незаметно исчерпать ресурсы веб-процесса. Если вы подключаете это к процессу публикации, в статье о вебхуках событий ссылок описано, как получать данные о кликах без опроса.
Какой вариант выбрать
Для задачи rake или разового скрипта: Net::HTTP.post, четыре строки - и готово. Для всего, что запускается по расписанию: вариант с Net::HTTP.start, тайм-аутами и ключом идемпотентности. Для приложения Rails, в котором уже есть Faraday: объект соединения с повторными попытками, объявленными один раз и используемыми повсюду.
Выбор почти не важен по сравнению с тремя базовыми привычками: брать ключ из окружения, явно задавать тайм-аут и проверять класс ответа до разбора тела. На странице API и SDK перечислены сгенерированные клиенты, если вы не хотите заниматься всем этим самостоятельно, а в разделе решений для разработчиков описано, что ещё предоставляет API, когда ссылки создаются из кода.
Изучите основную серию
Эта статья входит в кластер для разработчиков. Начните с руководства по бесплатному API сокращателя URL, чтобы разобраться с форматом эндпоинта и аутентификацией, затем прочитайте о лимитах и идемпотентности, чтобы корректно работать под нагрузкой. Актуальная справочная информация находится в документации API, а в статье сокращатели URL для разработчиков описано, что нужно проверить в API до начала разработки.
Другие статьи в блоге
Частые вопросы
Как сократить URL в Ruby?
Отправьте длинный URL методом POST в API сокращателя с помощью Net::HTTP, передав ключ API в заголовке Bearer, а целевой адрес - в теле JSON, затем прочитайте short_url из разобранного ответа. Net::HTTP.post записывается в одну строку и входит в стандартную библиотеку, поэтому для первой версии ничего устанавливать не нужно.
Нужен ли гем, чтобы сокращать URL в Ruby?
Нет. Net::HTTP и библиотека json входят в состав Ruby и покрывают весь вызов. Faraday оправдывает себя, когда вам нужны переиспользуемое соединение, middleware для повторных попыток и автоматическое кодирование JSON, что обычно актуально внутри приложения Rails, а не в отдельном скрипте.
Как установить тайм-аут для Net::HTTP?
Используйте Net::HTTP.start с open_timeout и read_timeout вместо сокращённого вызова Net::HTTP.post, который не позволяет задать ни один из них. Без этих параметров зависшее соединение может удерживать рабочий процесс все 60 секунд по умолчанию, а в фоновой задаче это означает потерю одного слота.
Почему Net::HTTP не выдаёт исключение для 401?
Потому что 401 - это корректный ответ HTTP, а не ошибка транспорта. Net::HTTP выдаёт исключения только при ошибках сокета и тайм-ауте, поэтому класс ответа нужно проверять самостоятельно с помощью is_a?(Net::HTTPSuccess) или читать response.code. Middleware raise_error в Faraday делает это за вас.
Как сократить сразу много URL в Ruby?
Поместите URL в Queue и запустите небольшой пул потоков, обычно примерно из восьми. Ruby освобождает глобальную блокировку, пока поток ждёт ввода-вывода, поэтому потоки действительно перекрывают сетевые ожидания, а фиксированный пул не даёт превысить лимит API, который неограниченный цикл «поток на URL» легко нарушил бы.
Где должен находиться цикл повторных попыток в приложении Rails?
В фоновой задаче, а не в действии контроллера. Повторная попытка с ожиданием в несколько секунд удерживает поток Puma всё это время, поэтому ограниченная лимитом партия может исчерпать ресурсы веб-процесса. Active Job с адаптером очереди обрабатывает задержку между попытками и бесплатно сохраняет историю повторов.
Попробуйте Elido
Вставьте URL - получите короткую ссылку
Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.
Бесплатно, без регистрации · 2 в день