6 min di letturaIngegneria

Come accorciare un URL in Ruby con Net::HTTP e Faraday

Accorcia un URL in Ruby con Net::HTTP e senza gem, poi aggiungi timeout, nuovi tentativi e idempotenza, oppure delega l'intero lavoro a Faraday in un'app Rails.

Marius Voß
DevRel · edge infra
Come accorciare un URL in Ruby: un POST di Net::HTTP invia un URL di destinazione a un'API di shortener e legge lo short link dal corpo della risposta JSON

Accorciare un URL in Ruby richiede un solo POST. Invia il link lungo all'API di uno shortener con Net::HTTP, passa la tua chiave API come token Bearer ed estrai short_url dal JSON restituito. Non serve alcuna gem, un aspetto importante quando lo script deve essere eseguito su una macchina che non controlli.

La struttura dell'endpoint e il modello di autenticazione qui sotto sono documentati nella panoramica dell'API gratuita per URL shortener; questo articolo è la versione specifica per Ruby della guida generale su come accorciare un URL, che invece illustra il percorso dalla dashboard.

Stessa serie, altri runtime: Python, JavaScript, PHP e Go. I nomi dei campi sono quelli di Elido; la struttura si adatta alla maggior parte degli shortener moderni.

Il modo più rapido: Net::HTTP.post

Due require, una chiamata:

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

Usare ENV.fetch invece di ENV[] è una scelta deliberata: se manca la chiave, lo script si interrompe subito invece di inviare Bearer e tornare con un 401 difficile da interpretare.

La riga unless response.is_a?(Net::HTTPSuccess) è quella che si tende a saltare. Net::HTTP genera un'eccezione in caso di socket non disponibile o timeout, e in nessun altro caso, quindi un 401 con un corpo di errore arriva con l'aspetto di un successo finché JSON.parse non ti restituisce un hash privo di short_url.

Aggiungere timeout, nuovi tentativi e una chiave di idempotenza

Net::HTTP.post ha un difetto che non puoi correggere: non accetta argomenti per il timeout. Per qualsiasi esecuzione non sorvegliata, passa a Net::HTTP.start, che li supporta.

Già che ci sei, gestisci il problema che un nuovo tentativo ingenuo può peggiorare. Se il POST raggiunge l'API ma la risposta si perde durante il ritorno, il codice va in timeout, riprova e crea un secondo link per la stessa destinazione. Una Idempotency-Key stabile chiude questa falla: calcola l'hash della destinazione e l'API restituisce il link originale invece di crearne uno nuovo.

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

Il case sulla classe della risposta è più leggibile di una serie di confronti sui codici di stato e rende evidente la politica: un 429 attende quanto richiesto dal server, un 5xx aumenta progressivamente l'attesa e qualsiasi altra risposta, come un 401 o un 422, genera un'eccezione al primo tentativo perché riprovare non risolverebbe il problema. L'approfondimento su rate limit e idempotenza descrive in dettaglio la semantica degli header.

Un POST di Net::HTTP in Ruby invia un URL di destinazione e una Idempotency-Key all'endpoint dei link dello shortener con un token Bearer; l'API restituisce HTTP 201 con short_url e un ciclo di nuovi tentativi attende le risposte 429 e 5xx

Vuoi eseguire il codice così com'è? Crea una chiave con il piano gratuito, esportala come ELIDO_API_KEY e tutti gli snippet di questa pagina funzioneranno senza modifiche.

La versione con Faraday per un'app che già possiedi

In un'app Rails, il ciclo scritto a mano ha la struttura sbagliata. Faraday ti offre un unico oggetto connessione con l'header di autenticazione associato, JSON in entrambe le direzioni e una politica di nuovi tentativi dichiarata invece che scritta:

# 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

Prima di incollare il codice, devi sapere due cose. In Faraday 2 il middleware per i nuovi tentativi è stato spostato nella gem separata faraday-retry, quindi serve una riga distinta nel Gemfile. Inoltre raise_error inverte il comportamento di Net::HTTP: ora 4xx e 5xx generano Faraday::ClientError e Faraday::ServerError, esattamente ciò che serve in un job che dovrebbe riprovare invece di memorizzare un valore nil.

Net::HTTP di Ruby a confronto con Faraday per chiamare un'API di URL shortener: uno script senza dipendenze contrapposto a un oggetto connessione che include il middleware per i nuovi tentativi in un'app Rails

Accorciare un elenco senza superare il rate limit

Ruby rilascia il lock globale mentre un thread attende l'IO, quindi qui i thread sono davvero vantaggiosi anche su CRuby. Quello che non devi fare è crearne uno per ogni URL: un CSV di 2.000 righe diventa 2.000 socket e una parete di 429. Una Queue insieme a un pool fisso mantiene costante la concorrenza, indipendentemente dalla lunghezza dell'elenco.

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

Indicizzare i risultati con l'URL originale mantiene visibile un errore parziale e consente di eseguire di nuovo il batch. Mutex non è opzionale: un semplice Hash scritto da otto thread costituisce una race condition e ti presenterà il conto proprio durante l'esecuzione che conta.

In Rails, inserisci shorten in un Active Job e lascia che sia l'adapter della coda a gestire l'attesa progressiva. Riprovare all'interno di un'azione del controller occupa un thread Puma durante ogni sleep e un'importazione soggetta a rate limit può saturare silenziosamente il processo web. Se stai integrando questo flusso in una pubblicazione, l'articolo sui webhook per gli eventi dei link spiega come recuperare i dati sui clic senza fare polling.

Quale scegliere

Un task rake o uno script occasionale: Net::HTTP.post, quattro righe e il gioco è fatto. Qualsiasi attività pianificata: la versione con Net::HTTP.start, timeout e chiave di idempotenza. Un'app Rails che usa già Faraday: l'oggetto connessione, con i nuovi tentativi dichiarati una volta e riutilizzati ovunque.

La scelta conta poco rispetto alle tre abitudini fondamentali. Leggere la chiave dall'ambiente, impostare un timeout esplicito e controllare la classe della risposta prima di analizzarne il corpo. La pagina API e SDK elenca i client generati se preferisci non occuparti di tutto questo, mentre le soluzioni per sviluppatori descrivono cos'altro espone l'API una volta che i link vengono creati dal codice.

Leggi la serie cornerstone

Questo articolo fa parte del cluster engineering. Inizia dalla guida all'API gratuita per URL shortener per conoscere struttura dell'endpoint e autenticazione, poi passa a rate limit e idempotenza per gestire correttamente il carico. Il riferimento aggiornato è la documentazione dell'API, mentre URL shortener per sviluppatori spiega cosa verificare in un'API prima di costruirci sopra.

Articoli correlati sul blog

Domande frequenti

Come posso accorciare un URL in Ruby?

Invia l'URL lungo all'API di uno shortener con Net::HTTP, passando la tua chiave API come header Bearer e la destinazione in un corpo JSON, poi leggi short_url dalla risposta analizzata. Net::HTTP.post è una chiamata su una sola riga nella libreria standard, quindi per la prima versione non devi installare nulla.

Mi serve una gem per accorciare gli URL in Ruby?

No. Net::HTTP e la libreria json sono incluse in Ruby e coprono l'intera chiamata. Faraday è utile quando vuoi una connessione riutilizzabile, un middleware per i nuovi tentativi e la codifica automatica in JSON, una situazione tipica all'interno di un'app Rails più che in uno script autonomo.

Come imposto un timeout su Net::HTTP?

Usa Net::HTTP.start con open_timeout e read_timeout invece della scorciatoia Net::HTTP.post, che non ti permette di impostare nessuno dei due. Senza questi valori, una connessione bloccata può tenere fermo un worker per tutti i 60 secondi predefiniti e, in un job in background, questo significa perdere uno slot di lavoro.

Perché Net::HTTP non genera un'eccezione per un 401?

Perché un 401 è una risposta HTTP valida, non un errore di trasporto. Net::HTTP genera eccezioni solo per errori di socket e timeout, quindi devi controllare personalmente la classe della risposta con is_a?(Net::HTTPSuccess) oppure leggere response.code. Il middleware raise_error di Faraday lo fa per te.

Come posso accorciare molti URL contemporaneamente in Ruby?

Inserisci gli URL in una Queue ed esegui un piccolo pool di thread che la elaborano, di solito circa otto. Ruby rilascia il lock globale durante l'IO, quindi i thread si sovrappongono davvero alle attese di rete, mentre un pool fisso ti mantiene sotto il rate limit dell'API che un ciclo senza limiti con un thread per URL supererebbe immediatamente.

Dove dovrebbe trovarsi il ciclo dei nuovi tentativi in un'app Rails?

In un job in background, non in un'azione del controller. Un nuovo tentativo che dorme per un paio di secondi occupa un thread Puma per tutto il tempo, quindi un batch soggetto a rate limit può saturare il processo web. Active Job con un adapter di coda gestisce l'attesa progressiva e ti offre gratuitamente la cronologia dei nuovi tentativi.

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Gratis, nessuna registrazione richiesta · 2 al giorno

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

Tag
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

Continua a leggere