5 min de leituraEngenharia

How to Shorten a URL in Ruby With Net::HTTP and Faraday

Shorten a URL in Ruby with Net::HTTP and no gems, then add timeouts, retries, and idempotency, or hand the whole job to Faraday in a Rails app.

Marius Voß
DevRel · edge infra
How to shorten a URL in Ruby: a Net::HTTP POST sending a destination URL to a shortener API and reading the short link back out of the JSON response

Shortening a URL in Ruby is one POST. Send the long link to a shortener's API with Net::HTTP, pass your API key as a Bearer token, and parse short_url out of the JSON that comes back. No gem required, which matters when the script has to run on a box you do not control.

The endpoint shape and auth model below are documented in the free URL shortener API overview; this post is the Ruby-specific version of the general how-to-shorten-a-url walkthrough, which covers the dashboard route instead.

Same series, other runtimes: Python, JavaScript, PHP, and Go. The field names are Elido's; the shape ports to most modern shorteners.

The Fastest Way: Net::HTTP.post

Two requires, one call:

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 rather than ENV[] is deliberate: a missing key blows up on the spot instead of sending Bearer and coming back with a confusing 401.

The unless response.is_a?(Net::HTTPSuccess) line is the one people skip. Net::HTTP raises on a dead socket or a timeout, and on nothing else, so a 401 with an error body arrives looking exactly like a success until JSON.parse hands you a hash with no short_url in it.

Add Timeouts, Retries, and an Idempotency Key

Net::HTTP.post has a flaw you cannot patch: it accepts no timeout arguments. For anything unattended, drop to Net::HTTP.start, which does.

While you are there, deal with the failure that a naive retry makes worse. If the POST reaches the API and the response is lost coming back, your code times out, retries, and creates a second link for the same destination. A stable Idempotency-Key closes that gap: hash the destination and the API returns the original link instead of minting a new one.

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

The case on response class reads better than a pile of status-code comparisons, and it makes the policy obvious: a 429 waits as long as the server asks, a 5xx backs off exponentially, and anything else, a 401 or a 422, raises on the first attempt because retrying will not repair it. The rate limits and idempotency deep-dive has the header semantics in full.

A Ruby Net::HTTP POST sending a destination URL and an Idempotency-Key to the shortener links endpoint with a Bearer token, the API returning HTTP 201 with short_url, and a retry loop that sleeps on 429 and 5xx responses

Want to run this as written? Create a key on the free plan, export it as ELIDO_API_KEY, and every snippet on this page works unchanged.

The Faraday Version for an App You Already Have

In a Rails app the hand-rolled loop is the wrong shape. Faraday gives you one connection object with the auth header attached, JSON in both directions, and a retry policy declared rather than written:

# 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

Two things to know before you paste that. The retry middleware moved to its own faraday-retry gem in Faraday 2, so it needs a separate Gemfile line. And raise_error inverts the Net::HTTP behaviour: 4xx and 5xx now raise Faraday::ClientError and Faraday::ServerError, which is what you want in a job that should retry rather than store a nil.

Ruby Net::HTTP compared with Faraday for calling a URL shortener API, contrasting a dependency-free script with a connection object that carries retry middleware in a Rails app

Shorten a List Without Tripping the Rate Limit

Ruby releases the global lock while a thread waits on IO, so threads are a real win here even on CRuby. What you must not do is spawn one per URL: a CSV of 2,000 rows becomes 2,000 sockets and a wall of 429s. A Queue plus a fixed pool keeps concurrency flat no matter how long the list is.

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

Keying results by the original URL keeps a partial failure visible and the batch re-runnable. The Mutex is not optional: a plain Hash written from eight threads is a data race, and it will bite you on the run that matters.

In Rails, wrap shorten in an Active Job and let the queue adapter own the backoff. Retrying inside a controller action holds a Puma thread through every sleep, and a rate-limited import can quietly starve the web process. If you are wiring this into a publish flow, the webhooks for link events post covers getting click data back out without polling.

Which One to Reach For

A rake task or a one-off script: Net::HTTP.post, four lines, done. Anything scheduled: the Net::HTTP.start version with timeouts and the idempotency key. A Rails app that already has Faraday: the connection object, with retries declared once and reused everywhere.

The choice barely matters compared to the three habits underneath it. Key from the environment, an explicit timeout, and a response class checked before the body is parsed. The API and SDKs page lists the generated clients if you would rather not own any of this, and solutions for developers covers what else the API exposes once links are being created from code.

Read the Cornerstone Series

This sits in the engineering cluster. Start with the free URL shortener API guide for the endpoint shape and auth, then rate limits and idempotency for behaving well under load. The live reference is the API docs, and URL shorteners for developers covers what to check in an API before you build on it.

Perguntas frequentes

How do I shorten a URL in Ruby?

POST the long URL to a shortener's API with Net::HTTP, sending your API key as a Bearer header and the destination in a JSON body, then read short_url from the parsed response. Net::HTTP.post is a one-liner in the standard library, so nothing needs installing for the first version.

Do I need a gem to shorten URLs in Ruby?

No. Net::HTTP and the json library ship with Ruby and cover the whole call. Faraday earns its place when you want a reusable connection, retry middleware, and automatic JSON encoding, which is usually the case inside a Rails app rather than a standalone script.

How do I set a timeout on Net::HTTP?

Use Net::HTTP.start with open_timeout and read_timeout rather than the Net::HTTP.post shortcut, which gives you no way to set either. Without them a stalled connection can hang a worker for the full 60-second default, and in a background job that means one lost job slot.

Why does Net::HTTP not raise on a 401?

Because a 401 is a valid HTTP response, not a transport failure. Net::HTTP only raises on socket and timeout errors, so you have to check the class of the response yourself with is_a?(Net::HTTPSuccess) or read response.code. Faraday's raise_error middleware does this for you.

How do I shorten many URLs at once in Ruby?

Push the URLs onto a Queue and run a small pool of threads against it, usually about eight. Ruby releases the global lock during IO, so threads genuinely overlap network waits, and a fixed pool keeps you under the API rate limit that an unbounded thread-per-URL loop would blow straight through.

Where should the retry loop live in a Rails app?

In a background job, not in a controller action. A retry that sleeps for a couple of seconds holds a Puma thread the whole time, so a rate-limited batch can starve the web process. Active Job with a queue adapter handles the backoff and gives you a retry history for free.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Grátis, sem necessidade de registo · 2 por dia

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

Tags
how to shorten a url in ruby
ruby url shortener
shorten url ruby net http
url shortener api ruby
faraday post json
bulk shorten urls ruby

Continuar lendo