5 Min. LesezeitEngineering

Eine URL in Ruby mit Net::HTTP und Faraday kürzen

Kürzen Sie eine URL in Ruby mit Net::HTTP und ohne Gems, fügen Sie dann Timeouts, Retries und Idempotenz hinzu oder überlassen Sie die gesamte Aufgabe in einer Rails-App Faraday.

Marius Voß
DevRel · edge infra
Eine URL in Ruby kürzen: Ein Net::HTTP-POST sendet eine Ziel-URL an eine Shortener-API und liest den Kurzlink aus der JSON-Antwort zurück

Eine URL in Ruby zu kürzen ist ein einziger POST. Senden Sie den langen Link mit Net::HTTP an die API eines Shorteners, übergeben Sie Ihren API-Schlüssel als Bearer-Token und parsen Sie short_url aus der zurückgegebenen JSON-Antwort. Dafür ist kein Gem erforderlich. Das ist wichtig, wenn das Skript auf einem Rechner laufen muss, den Sie nicht kontrollieren.

Das Format des Endpunkts und das Authentifizierungsmodell weiter unten sind in der Übersicht zur kostenlosen URL-Shortener-API dokumentiert. Dieser Beitrag ist die Ruby-spezifische Version des allgemeinen Leitfadens zum Kürzen einer URL, der stattdessen den Weg über das Dashboard behandelt.

Dieselbe Serie, andere Laufzeitumgebungen: Python, JavaScript, PHP und Go. Die Feldnamen stammen von Elido; das Format lässt sich auf die meisten modernen Shortener übertragen.

Der schnellste Weg: Net::HTTP.post

Zwei Requires, ein Aufruf:

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 statt ENV[] zu verwenden, ist Absicht: Ein fehlender Schlüssel führt sofort zu einem Fehler, statt Bearer zu senden und mit einem verwirrenden 401 zurückzukommen.

Die Zeile unless response.is_a?(Net::HTTPSuccess) überspringen viele. Net::HTTP löst bei einem nicht erreichbaren Socket oder einem Timeout eine Exception aus, sonst aber bei nichts. Daher sieht eine 401-Antwort mit Fehler-Body genauso wie ein Erfolg aus, bis JSON.parse Ihnen einen Hash ohne short_url liefert.

Timeouts, Retries und einen Idempotency-Key hinzufügen

Net::HTTP.post hat einen Fehler, den Sie nicht beheben können: Es akzeptiert keine Timeout-Argumente. Für alles, was unbeaufsichtigt läuft, wechseln Sie zu Net::HTTP.start, das diese Argumente unterstützt.

Kümmern Sie sich dabei um den Fehler, den ein naiver Retry noch verschlimmert. Wenn der POST die API erreicht, aber die Antwort auf dem Rückweg verloren geht, läuft Ihr Code in einen Timeout, wiederholt den Aufruf und erstellt einen zweiten Link für dasselbe Ziel. Ein stabiler Idempotency-Key schließt diese Lücke: Hashen Sie das Ziel, und die API gibt den ursprünglichen Link zurück, statt einen neuen zu erzeugen.

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

Der case-Ausdruck nach der Antwortklasse ist übersichtlicher als eine Ansammlung von Statuscode-Vergleichen und macht die Richtlinie deutlich: Bei einem 429 wartet der Code so lange, wie der Server vorgibt, bei einem 5xx wird das Backoff exponentiell verlängert, und alles andere, etwa ein 401 oder 422, löst beim ersten Versuch eine Exception aus, weil ein Retry den Fehler nicht beheben wird. Der Deep-Dive zu Ratenlimits und Idempotenz erläutert die Semantik der Header vollständig.

Ein Ruby-Net::HTTP-POST sendet eine Ziel-URL und einen Idempotency-Key mit einem Bearer-Token an den Links-Endpunkt des Shorteners. Die API gibt HTTP 201 mit short_url zurück, und eine Retry-Schleife wartet bei 429- und 5xx-Antworten

Möchten Sie den Code genau so ausführen? Erstellen Sie einen Schlüssel im kostenlosen Tarif, exportieren Sie ihn als ELIDO_API_KEY, und jedes Snippet auf dieser Seite funktioniert unverändert.

Die Faraday-Version für eine vorhandene App

In einer Rails-App hat die von Hand geschriebene Schleife die falsche Form. Faraday liefert Ihnen ein Verbindungsobjekt mit angehängtem Authentifizierungs-Header, JSON in beide Richtungen und eine deklarierte statt ausprogrammierte Retry-Richtlinie:

# 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

Zwei Dinge sollten Sie wissen, bevor Sie den Code einfügen. Die Retry-Middleware ist in Faraday 2 in ein eigenes faraday-retry-Gem verschoben worden, daher benötigt sie eine separate Zeile im Gemfile. Und raise_error kehrt das Verhalten von Net::HTTP um: 4xx und 5xx lösen jetzt Faraday::ClientError und Faraday::ServerError aus. Das ist für einen Job sinnvoll, der einen Retry ausführen soll, statt einen nil-Wert zu speichern.

Ruby Net::HTTP im Vergleich zu Faraday für den Aufruf einer URL-Shortener-API: ein Skript ohne Abhängigkeiten gegenüber einem Verbindungsobjekt mit Retry-Middleware in einer Rails-App

Eine Liste kürzen, ohne das Ratenlimit zu überschreiten

Ruby gibt die globale Sperre frei, während ein Thread auf I/O wartet. Daher sind Threads hier selbst unter CRuby ein echter Vorteil. Sie dürfen nur nicht einen Thread pro URL starten: Aus einer CSV-Datei mit 2.000 Zeilen werden 2.000 Sockets und eine Wand aus 429-Antworten. Eine Queue mit einem festen Pool hält die Parallelität unabhängig von der Listenlänge konstant.

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

Wenn Sie die Ergebnisse nach der ursprünglichen URL indizieren, bleiben Teilausfälle sichtbar und der Batch kann erneut ausgeführt werden. Der Mutex ist nicht optional: Ein einfacher Hash, in den acht Threads schreiben, erzeugt eine Race Condition, die Sie genau bei dem Lauf treffen wird, auf den es ankommt.

Verpacken Sie shorten in Rails in einen Active Job und überlassen Sie das Backoff dem Queue-Adapter. Ein Retry innerhalb einer Controller-Aktion hält bei jedem sleep einen Puma-Thread fest, und ein gedrosselter Import kann den Webprozess unbemerkt aushungern. Wenn Sie das in einen Veröffentlichungsablauf integrieren, erklärt der Beitrag zu Webhooks für Link-Ereignisse, wie Sie Klickdaten ohne Polling zurückerhalten.

Welche Variante Sie wählen sollten

Für eine Rake-Aufgabe oder ein einmaliges Skript: Net::HTTP.post, vier Zeilen, fertig. Für alles Geplante: die Net::HTTP.start-Version mit Timeouts und Idempotency-Key. Für eine Rails-App, die bereits Faraday verwendet: das Verbindungsobjekt mit einmal deklarierten und überall wiederverwendeten Retries.

Die Wahl ist im Vergleich zu den drei grundlegenden Gewohnheiten kaum entscheidend: den Schlüssel aus der Umgebung beziehen, ein explizites Timeout setzen und die Antwortklasse prüfen, bevor der Body geparst wird. Die Seite APIs und SDKs listet die generierten Clients auf, falls Sie all das lieber nicht selbst verwalten möchten. Lösungen für Entwickler zeigt, was die API noch anbietet, sobald Links aus Code heraus erstellt werden.

Die Cornerstone-Serie lesen

Dieser Beitrag gehört zum Engineering-Cluster. Beginnen Sie mit dem Leitfaden zur kostenlosen URL-Shortener-API, um das Endpunktformat und die Authentifizierung kennenzulernen, und lesen Sie dann Ratenlimits und Idempotenz, um sich unter Last korrekt zu verhalten. Die aktuelle Referenz sind die API-Dokumente, und URL-Shortener für Entwickler erklärt, was Sie bei einer API prüfen sollten, bevor Sie darauf aufbauen.

Verwandte Beiträge im Blog

Häufig gestellte Fragen

Wie kürze ich eine URL in Ruby?

Senden Sie die lange URL mit Net::HTTP per POST an die API eines Shorteners, übergeben Sie Ihren API-Schlüssel als Bearer-Header und das Ziel in einem JSON-Body, und lesen Sie anschließend short_url aus der geparsten Antwort. Net::HTTP.post ist eine einzige Zeile in der Standardbibliothek, daher muss für die erste Version nichts installiert werden.

Brauche ich ein Gem, um URLs in Ruby zu kürzen?

Nein. Net::HTTP und die json-Bibliothek werden mit Ruby ausgeliefert und decken den gesamten Aufruf ab. Faraday lohnt sich, wenn Sie eine wiederverwendbare Verbindung, Retry-Middleware und automatische JSON-Kodierung möchten. Das ist in der Regel in einer Rails-App der Fall, nicht in einem eigenständigen Skript.

Wie setze ich bei Net::HTTP ein Timeout?

Verwenden Sie Net::HTTP.start mit open_timeout und read_timeout statt der Abkürzung Net::HTTP.post, bei der Sie keines von beiden setzen können. Ohne diese Werte kann eine hängende Verbindung einen Worker die vollen 60 Sekunden des Standardwerts blockieren, und in einem Hintergrundjob bedeutet das einen verlorenen Job-Slot.

Warum löst Net::HTTP bei einem 401 keine Exception aus?

Weil ein 401 eine gültige HTTP-Antwort und kein Transportfehler ist. Net::HTTP löst nur bei Socket- und Timeout-Fehlern eine Exception aus. Sie müssen die Klasse der Antwort daher selbst mit is_a?(Net::HTTPSuccess) prüfen oder response.code auslesen. Faradays raise_error-Middleware erledigt das für Sie.

Wie kürze ich viele URLs auf einmal in Ruby?

Legen Sie die URLs in eine Queue und lassen Sie einen kleinen Thread-Pool, normalerweise mit etwa acht Threads, darauf arbeiten. Ruby gibt die globale Sperre während I/O frei, sodass Threads Wartezeiten im Netzwerk tatsächlich überlappen. Ein fester Pool hält Sie unter dem API-Ratenlimit, das eine unbegrenzte Schleife mit einem Thread pro URL sofort sprengen würde.

Wo sollte die Retry-Schleife in einer Rails-App laufen?

In einem Hintergrundjob, nicht in einer Controller-Aktion. Ein Retry, der einige Sekunden schläft, hält währenddessen einen Puma-Thread fest, sodass ein gedrosselter Batch den Webprozess aushungern kann. Active Job mit einem Queue-Adapter übernimmt das Backoff und liefert Ihnen kostenlos eine Retry-Historie.

Elido testen

URL einfügen, kurzer Link in Sekunden

Kein Konto nötig. Link bleibt 30 Tage aktiv. Konto erstellen, um ihn dauerhaft zu behalten.

Kostenlos, keine Anmeldung erforderlich · 2 pro Tag

Elido testen

URL-Shortener mit EU-Hosting: eigene Domains, tiefe Analytik und eine offene API. Kostenloser Tarif - keine Kreditkarte nötig.

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

Weiterlesen