6 min de leituraEngenharia

Como encurtar uma URL em Ruby com Net::HTTP e Faraday

Encurte uma URL em Ruby com Net::HTTP e sem gems, depois adicione timeouts, retries e idempotência, ou deixe todo o trabalho por conta do Faraday em um app Rails.

Marius Voß
DevRel · edge infra
Como encurtar uma URL em Ruby: um POST de Net::HTTP envia uma URL de destino para uma API de encurtador e lê o link curto de volta na resposta JSON

Encurtar uma URL em Ruby é um único POST. Envie o link longo para a API de um encurtador com Net::HTTP, passe sua chave de API como um token Bearer e extraia short_url do JSON retornado. Nenhuma gem é necessária, o que importa quando o script precisa rodar em uma máquina que você não controla.

O formato do endpoint e o modelo de autenticação abaixo estão documentados na visão geral da API gratuita de encurtador de URLs; este post é a versão específica para Ruby do guia geral sobre como encurtar uma URL, que aborda o caminho pelo dashboard.

Mesma série, outros runtimes: Python, JavaScript, PHP e Go. Os nomes dos campos são do Elido; o formato pode ser adaptado à maioria dos encurtadores modernos.

A Maneira Mais Rápida: Net::HTTP.post

Dois requires, uma chamada:

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 em vez de ENV[] é intencional: uma chave ausente falha imediatamente, em vez de enviar Bearer e voltar com um 401 confuso.

A linha unless response.is_a?(Net::HTTPSuccess) é a que as pessoas costumam ignorar. O Net::HTTP gera uma exceção em um socket inativo ou timeout, e em nenhuma outra situação, então uma resposta 401 com um corpo de erro chega parecendo exatamente um sucesso até que JSON.parse entregue um hash sem short_url.

Adicione Timeouts, Retries e uma Chave de Idempotência

Net::HTTP.post tem uma falha que você não pode corrigir: ele não aceita argumentos de timeout. Para qualquer coisa que rode sem supervisão, use Net::HTTP.start, que aceita esses argumentos.

Já que está fazendo isso, trate também da falha que um retry ingênuo piora. Se o POST chegar à API e a resposta se perder no caminho de volta, seu código atingirá o timeout, fará retry e criará um segundo link para o mesmo destino. Uma Idempotency-Key estável fecha essa brecha: faça o hash do destino e a API retornará o link original em vez de criar um novo.

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

O case sobre a classe da resposta é mais fácil de ler do que uma pilha de comparações de códigos de status e deixa a política evidente: um 429 espera o tempo solicitado pelo servidor, um 5xx usa backoff exponencial e qualquer outra coisa, como um 401 ou 422, gera uma exceção na primeira tentativa porque o retry não vai corrigi-la. O guia detalhado sobre limites de taxa e idempotência explica toda a semântica dos cabeçalhos.

Um POST de Ruby com Net::HTTP envia uma URL de destino e uma Idempotency-Key ao endpoint de links do encurtador com um token Bearer; a API retorna HTTP 201 com short_url e um loop de retry espera em respostas 429 e 5xx

Quer executar isso exatamente como está? Crie uma chave no plano gratuito, exporte-a como ELIDO_API_KEY e todos os snippets desta página funcionarão sem alterações.

A Versão com Faraday para um App que Você Já Tem

Em um app Rails, o loop feito à mão tem o formato errado. O Faraday fornece um único objeto de conexão com o cabeçalho de autenticação anexado, JSON nos dois sentidos e uma política de retry declarada em vez de escrita manualmente:

# 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

Duas coisas que você precisa saber antes de colar isso. O middleware de retry foi movido para uma gem própria, faraday-retry, no Faraday 2, então é necessária uma linha separada no Gemfile. E raise_error inverte o comportamento do Net::HTTP: agora 4xx e 5xx geram Faraday::ClientError e Faraday::ServerError, que é o que você quer em um job que deve fazer retry em vez de armazenar um nil.

Ruby Net::HTTP comparado ao Faraday para chamar uma API de encurtador de URLs, contrastando um script sem dependências com um objeto de conexão que carrega middleware de retry em um app Rails

Encurte uma Lista sem Estourar o Limite de Taxa

O Ruby libera o bloqueio global enquanto uma thread espera pela E/S, então as threads são uma vantagem real aqui até mesmo no CRuby. O que você não deve fazer é criar uma por URL: um CSV com 2.000 linhas vira 2.000 sockets e uma parede de 429s. Uma Queue mais um pool fixo mantém a concorrência constante, independentemente do tamanho da lista.

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

Usar a URL original como chave dos resultados mantém uma falha parcial visível e permite executar o lote novamente. O Mutex não é opcional: um Hash comum escrito por oito threads é uma condição de corrida, e isso vai atingir você justamente na execução importante.

No Rails, envolva shorten em um Active Job e deixe o adaptador de fila cuidar do backoff. Fazer retry dentro de uma ação de controller mantém uma thread do Puma ocupada durante cada sleep, e uma importação limitada por taxa pode esgotar silenciosamente o processo web. Se você estiver integrando isso a um fluxo de publicação, o post sobre webhooks para eventos de links explica como obter os dados de cliques de volta sem polling.

Qual Escolher

Uma tarefa do rake ou um script pontual: Net::HTTP.post, quatro linhas e pronto. Qualquer coisa agendada: a versão com Net::HTTP.start, timeouts e a chave de idempotência. Um app Rails que já usa Faraday: o objeto de conexão, com retries declarados uma vez e reutilizados em todos os lugares.

A escolha importa pouco em comparação com os três hábitos por baixo dela. Obter a chave do ambiente, definir um timeout explícito e verificar a classe da resposta antes de analisar o corpo. A página A API e os SDKs lista os clientes gerados caso você prefira não cuidar de nada disso, e soluções para desenvolvedores mostra o que mais a API oferece quando os links passam a ser criados pelo código.

Leia a Série Principal

Este artigo faz parte do cluster de engenharia. Comece pelo guia da API gratuita de encurtador de URLs para entender o formato do endpoint e a autenticação, depois leia limites de taxa e idempotência para agir corretamente sob carga. A referência ao vivo está na documentação da API, e encurtadores de URL para desenvolvedores explica o que verificar em uma API antes de construir sobre ela.

Relacionado no Blog

Perguntas frequentes

Como encurto uma URL em Ruby?

Faça um POST da URL longa para a API de um encurtador com Net::HTTP, enviando sua chave de API como um cabeçalho Bearer e o destino em um corpo JSON, depois leia short_url da resposta analisada. Net::HTTP.post é uma chamada de uma linha na biblioteca padrão, então nada precisa ser instalado na primeira versão.

Preciso de uma gem para encurtar URLs em Ruby?

Não. Net::HTTP e a biblioteca json vêm com o Ruby e cobrem toda a chamada. O Faraday vale a pena quando você quer uma conexão reutilizável, middleware de retry e codificação JSON automática, o que costuma acontecer dentro de um app Rails, e não em um script independente.

Como defino um timeout no Net::HTTP?

Use Net::HTTP.start com open_timeout e read_timeout em vez do atalho Net::HTTP.post, que não permite definir nenhum dos dois. Sem eles, uma conexão travada pode prender um worker pelo padrão completo de 60 segundos e, em um job em segundo plano, isso significa uma vaga de job perdida.

Por que o Net::HTTP não gera uma exceção para um 401?

Porque um 401 é uma resposta HTTP válida, não uma falha de transporte. O Net::HTTP só gera exceções para erros de socket e timeout, então você precisa verificar a classe da resposta por conta própria com is_a?(Net::HTTPSuccess) ou ler response.code. O middleware raise_error do Faraday faz isso por você.

Como encurto muitas URLs de uma vez em Ruby?

Coloque as URLs em uma Queue e execute um pequeno pool de threads sobre ela, geralmente com cerca de oito. O Ruby libera o bloqueio global durante a E/S, então as threads realmente se sobrepõem enquanto esperam a rede, e um pool fixo mantém você abaixo do limite de taxa da API, que um loop ilimitado com uma thread por URL ultrapassaria imediatamente.

Onde o loop de retry deve ficar em um app Rails?

Em um job em segundo plano, não em uma ação de controller. Um retry que dorme por alguns segundos mantém uma thread do Puma ocupada durante todo esse tempo, então um lote limitado por taxa pode esgotar o processo web. O Active Job com um adaptador de fila cuida do backoff e fornece um histórico de retry automaticamente.

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