6 min de lectureIngénierie

Comment raccourcir une URL en Ruby avec Net::HTTP et Faraday

Raccourcissez une URL en Ruby avec Net::HTTP et sans gem, puis ajoutez des délais d'expiration, des nouvelles tentatives et l'idempotence, ou confiez toute la tâche à Faraday dans une application Rails.

Marius Voß
DevRel · edge infra
Comment raccourcir une URL en Ruby : un POST Net::HTTP qui envoie une URL de destination à une API de raccourcissement et lit le lien court dans la réponse JSON

Raccourcir une URL en Ruby tient en un seul POST. Envoyez le lien long à l'API d'un raccourcisseur avec Net::HTTP, transmettez votre clé API comme jeton Bearer et extrayez short_url du JSON renvoyé. Aucune gem n'est nécessaire, ce qui compte lorsque le script doit s'exécuter sur une machine que vous ne contrôlez pas.

La forme du point de terminaison et le modèle d'authentification ci-dessous sont documentés dans la présentation de l'API gratuite de raccourcissement d'URL ; cet article est la version propre à Ruby du guide général pour raccourcir une URL, qui couvre plutôt le parcours depuis le tableau de bord.

La même série, avec d'autres environnements d'exécution : Python, JavaScript, PHP et Go. Les noms de champs sont ceux d'Elido ; la structure s'adapte à la plupart des raccourcisseurs modernes.

La méthode la plus rapide : Net::HTTP.post

Deux require, un appel :

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

Choisir ENV.fetch plutôt que ENV[] est volontaire : une clé manquante provoque immédiatement une erreur au lieu d'envoyer Bearer et de revenir avec un 401 déroutant.

La ligne unless response.is_a?(Net::HTTPSuccess) est celle que beaucoup omettent. Net::HTTP lève une exception sur un socket fermé ou un délai d'expiration, et sur rien d'autre ; une réponse 401 avec un corps d'erreur arrive donc comme si elle indiquait un succès jusqu'à ce que JSON.parse vous renvoie un hash sans short_url.

Ajouter des délais d'expiration, des nouvelles tentatives et une clé d'idempotence

Net::HTTP.post présente un défaut impossible à corriger : il n'accepte aucun argument de délai d'expiration. Pour tout ce qui doit s'exécuter sans surveillance, utilisez Net::HTTP.start, qui le permet.

Profitez-en pour traiter l'échec qu'une nouvelle tentative naïve aggrave. Si le POST atteint l'API mais que la réponse se perd sur le chemin du retour, votre code arrive au délai d'expiration, effectue une nouvelle tentative et crée un second lien pour la même destination. Une Idempotency-Key stable comble cette lacune : hachez la destination et l'API renverra le lien d'origine au lieu d'en créer un nouveau.

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

Le case sur la classe de la réponse est plus lisible qu'une accumulation de comparaisons de codes d'état, et il rend la politique évidente : un 429 attend aussi longtemps que le demande le serveur, un 5xx applique un backoff exponentiel, et tout le reste, notamment un 401 ou un 422, lève une exception dès la première tentative, car une nouvelle tentative ne réparera rien. L'analyse approfondie des limites de débit et de l'idempotence détaille la sémantique des en-têtes.

Un POST Ruby Net::HTTP qui envoie une URL de destination et une Idempotency-Key au point de terminaison des liens du raccourcisseur avec un jeton Bearer, l'API renvoyant HTTP 201 avec short_url, et une boucle de nouvelles tentatives qui attend lors des réponses 429 et 5xx

Vous voulez l'exécuter tel quel ? Créez une clé avec le forfait gratuit, exportez-la sous ELIDO_API_KEY et chaque extrait de code de cette page fonctionnera sans modification.

La version Faraday pour une application existante

Dans une application Rails, la boucle écrite à la main n'est pas adaptée. Faraday vous fournit un objet de connexion unique avec l'en-tête d'authentification déjà attaché, du JSON dans les deux sens et une politique de nouvelles tentatives déclarée plutôt qu'écrite :

# 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

Deux choses à savoir avant de coller ce code. Le middleware de nouvelles tentatives a été déplacé dans sa propre gem faraday-retry avec Faraday 2, et nécessite donc une ligne distincte dans le Gemfile. De plus, raise_error inverse le comportement de Net::HTTP : les erreurs 4xx et 5xx lèvent désormais Faraday::ClientError et Faraday::ServerError, ce qui est souhaitable dans un job qui doit effectuer une nouvelle tentative plutôt que stocker une valeur nil.

Net::HTTP Ruby comparé à Faraday pour appeler une API de raccourcissement d'URL, avec d'un côté un script sans dépendance et de l'autre un objet de connexion qui embarque un middleware de nouvelles tentatives dans une application Rails

Raccourcir une liste sans dépasser la limite de débit

Ruby libère le verrou global lorsqu'un thread attend une E/S, si bien que les threads sont réellement avantageux ici, même avec CRuby. Ce qu'il ne faut pas faire, c'est en créer un par URL : un CSV de 2 000 lignes devient 2 000 sockets et un mur de 429. Une Queue accompagnée d'un pool fixe maintient la concurrence à un niveau constant, quelle que soit la longueur de la liste.

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

Indexer les résultats avec l'URL d'origine rend visible un échec partiel et permet de relancer le lot. Le Mutex n'est pas facultatif : un Hash ordinaire écrit par huit threads constitue une course de données, qui vous le fera payer lors de l'exécution qui compte.

Dans Rails, encapsulez shorten dans un Active Job et laissez l'adaptateur de file d'attente gérer le backoff. Effectuer une nouvelle tentative dans une action de contrôleur monopolise un thread Puma pendant chaque sleep, et un import limité par le débit peut discrètement affamer le processus web. Si vous intégrez cela dans un flux de publication, l'article sur les webhooks pour les événements de liens explique comment récupérer les données de clic sans interrogation périodique.

Lequel choisir

Pour une tâche rake ou un script ponctuel : Net::HTTP.post, quatre lignes, et c'est terminé. Pour tout ce qui est planifié : la version avec Net::HTTP.start, délais d'expiration et clé d'idempotence. Pour une application Rails qui utilise déjà Faraday : l'objet de connexion, avec les nouvelles tentatives déclarées une fois puis réutilisées partout.

Le choix compte peu face aux trois habitudes sous-jacentes. Prenez la clé dans l'environnement, définissez un délai d'expiration explicite et vérifiez la classe de la réponse avant d'analyser le corps. La page API et SDK répertorie les clients générés si vous préférez ne rien gérer de tout cela, et les solutions pour les développeurs présentent les autres fonctions exposées par l'API une fois que les liens sont créés depuis du code.

Lire la série pilier

Cet article appartient au cluster engineering. Commencez par le guide de l'API gratuite de raccourcissement d'URL pour la forme du point de terminaison et l'authentification, puis consultez les limites de débit et l'idempotence pour bien vous comporter sous charge. La référence à jour se trouve dans la documentation de l'API, et les raccourcisseurs d'URL pour les développeurs indiquent les points à vérifier dans une API avant de construire dessus.

Articles associés sur le blog

Questions fréquentes

Comment raccourcir une URL en Ruby ?

Envoyez l'URL longue à l'API d'un raccourcisseur avec Net::HTTP, en transmettant votre clé API dans un en-tête Bearer et la destination dans un corps JSON, puis lisez short_url dans la réponse analysée. Net::HTTP.post tient sur une seule ligne dans la bibliothèque standard, donc rien n'est à installer pour la première version.

Ai-je besoin d'une gem pour raccourcir des URL en Ruby ?

Non. Net::HTTP et la bibliothèque json sont fournies avec Ruby et couvrent tout l'appel. Faraday devient utile lorsque vous voulez une connexion réutilisable, un middleware de nouvelles tentatives et un encodage JSON automatique, ce qui est généralement le cas dans une application Rails plutôt que dans un script autonome.

Comment définir un délai d'expiration avec Net::HTTP ?

Utilisez Net::HTTP.start avec open_timeout et read_timeout plutôt que le raccourci Net::HTTP.post, qui ne permet de définir ni l'un ni l'autre. Sans ces paramètres, une connexion bloquée peut immobiliser un worker pendant les 60 secondes du délai par défaut, ce qui signifie perdre un emplacement de job dans une tâche en arrière-plan.

Pourquoi Net::HTTP ne lève-t-il pas d'exception pour un 401 ?

Parce qu'un 401 est une réponse HTTP valide, et non une défaillance du transport. Net::HTTP ne lève des exceptions que pour les erreurs de socket et de délai d'expiration ; vous devez donc vérifier vous-même la classe de la réponse avec is_a?(Net::HTTPSuccess) ou lire response.code. Le middleware raise_error de Faraday s'en charge pour vous.

Comment raccourcir de nombreuses URL en une seule fois en Ruby ?

Placez les URL dans une Queue et exécutez un petit pool de threads, généralement autour de huit, pour les traiter. Ruby libère le verrou global pendant les E/S, les threads peuvent donc réellement superposer les attentes réseau, et un pool fixe vous maintient sous la limite de débit de l'API, qu'une boucle illimitée avec un thread par URL dépasserait immédiatement.

Où la boucle de nouvelles tentatives doit-elle se trouver dans une application Rails ?

Dans un job en arrière-plan, pas dans une action de contrôleur. Une nouvelle tentative qui attend quelques secondes monopolise un thread Puma pendant tout ce temps, si bien qu'un lot limité par le débit peut affamer le processus web. Active Job et un adaptateur de file d'attente gèrent le backoff et vous donnent gratuitement un historique des nouvelles tentatives.

Essayer Elido

Collez une URL, obtenez un lien court

Sans inscription. Lien actif 30 jours. Inscrivez-vous pour le garder pour toujours.

Gratuit, sans inscription · 2 par jour

Essayer Elido

Raccourcisseur d'URL hébergé en UE : domaines personnalisés, analyses approfondies et API ouverte. Forfait gratuit - sans carte bancaire.

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

Lire la suite