5 min czytaniaInżynieria

Jak skrócić URL w Ruby za pomocą Net::HTTP i Faraday

Skróć URL w Ruby za pomocą Net::HTTP bez gemów, następnie dodaj limity czasu, ponowienia i idempotencję albo przekaż całe zadanie Faraday w aplikacji Rails.

Marius Voß
DevRel · edge infra
Jak skrócić URL w Ruby: żądanie POST Net::HTTP wysyłające docelowy URL do API skracacza i odczytujące krótki link z odpowiedzi JSON

Skrócenie URL-a w Ruby to jedno żądanie POST. Wyślij długi link do API skracacza za pomocą Net::HTTP, przekaż klucz API jako token Bearer i odczytaj short_url z otrzymanego JSON-a. Gem nie jest potrzebny, co ma znaczenie, gdy skrypt musi działać na maszynie, nad którą nie masz kontroli.

Opis endpointu i modelu uwierzytelniania znajduje się w przeglądzie bezpłatnego API skracacza URL; ten artykuł jest wersją dotyczącą Ruby ogólnego przewodnika jak skrócić URL, który omawia ścieżkę przez dashboard.

Ta sama seria dla innych środowisk uruchomieniowych: Python, JavaScript, PHP i Go. Nazwy pól pochodzą z Elido; ten kształt można przenieść do większości nowoczesnych skracaczy.

Najszybszy sposób: Net::HTTP.post

Dwa requires, jedno wywołanie:

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 zamiast ENV[] jest użyte celowo: brakujący klucz powoduje natychmiastowe przerwanie działania, zamiast wysłania Bearer i powrotu z mylącym 401.

Wiersz unless response.is_a?(Net::HTTPSuccess) jest tym, co wiele osób pomija. Net::HTTP zgłasza wyjątek przy niedziałającym gnieździe lub przekroczeniu limitu czasu i przy niczym więcej, więc 401 z treścią błędu dociera i wygląda dokładnie jak sukces, dopóki JSON.parse nie zwróci hasha bez short_url.

Dodaj limity czasu, ponowienia i klucz idempotencji

Net::HTTP.post ma wadę, której nie da się obejść: nie przyjmuje argumentów określających limit czasu. W przypadku czegokolwiek działającego bez nadzoru przejdź na Net::HTTP.start, które je obsługuje.

Przy okazji zajmij się błędem, który naiwne ponowienie pogarsza. Jeśli żądanie POST dotrze do API, ale odpowiedź zaginie po drodze, kod przekroczy limit czasu, ponowi żądanie i utworzy drugi link dla tego samego miejsca docelowego. Stały Idempotency-Key zamyka tę lukę: zahaszuj miejsce docelowe, a API zwróci oryginalny link zamiast tworzyć nowy.

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

Instrukcja case dotycząca klasy odpowiedzi jest czytelniejsza niż stos porównań kodów statusu i jasno pokazuje zasady: 429 czeka tyle, ile żąda serwer, 5xx stosuje wykładnicze narastanie opóźnienia, a wszystko inne, na przykład 401 lub 422, powoduje wyjątek przy pierwszej próbie, ponieważ ponowienie tego nie naprawi. Szczegółowe omówienie limitów szybkości i idempotencji wyjaśnia znaczenie nagłówków.

Żądanie POST Ruby Net::HTTP wysyłające docelowy URL i Idempotency-Key do endpointu linków skracacza z tokenem Bearer, API zwracające HTTP 201 z short_url oraz pętla ponowień wykonująca sleep dla odpowiedzi 429 i 5xx

Chcesz uruchomić ten kod bez zmian? Utwórz klucz w bezpłatnym planie, wyeksportuj go jako ELIDO_API_KEY, a każdy fragment kodu na tej stronie zadziała bez modyfikacji.

Wersja z Faraday dla istniejącej aplikacji

W aplikacji Rails ręcznie napisana pętla ma niewłaściwy kształt. Faraday udostępnia jeden obiekt połączenia z dołączonym nagłówkiem uwierzytelniania, JSON-em w obu kierunkach i zadeklarowaną polityką ponowień zamiast kodowania jej ręcznie:

# 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

Przed wklejeniem tego kodu warto wiedzieć dwie rzeczy. Middleware ponowień przeniesiono w Faraday 2 do osobnego gema faraday-retry, więc potrzebny jest oddzielny wiersz w Gemfile. A raise_error odwraca zachowanie Net::HTTP: teraz 4xx i 5xx zgłaszają Faraday::ClientError i Faraday::ServerError, czego potrzebujesz w zadaniu, które powinno ponowić działanie zamiast zapisać nil.

Net::HTTP w Ruby w porównaniu z Faraday przy wywoływaniu API skracacza URL: skrypt bez zależności oraz obiekt połączenia z middleware ponowień w aplikacji Rails

Skróć listę bez przekraczania limitu szybkości

Ruby zwalnia globalną blokadę, gdy wątek czeka na operację wejścia-wyjścia, więc wątki naprawdę się tutaj opłacają nawet w CRuby. Nie wolno jednak uruchamiać jednego wątku na URL: CSV z 2000 wierszy zamienia się w 2000 gniazd i ścianę odpowiedzi 429. Queue oraz stała pula utrzymują stałą współbieżność niezależnie od długości listy.

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

Indeksowanie wyników za pomocą oryginalnego URL-a sprawia, że częściowa awaria pozostaje widoczna, a partię można uruchomić ponownie. Mutex nie jest opcjonalny: zwykły Hash zapisywany z ośmiu wątków tworzy wyścig danych i da o sobie znać podczas tego uruchomienia, na którym najbardziej Ci zależy.

W Rails opakuj shorten w Active Job i pozwól adapterowi kolejki zarządzać narastaniem opóźnienia. Ponowienie wewnątrz akcji kontrolera zajmuje wątek Puma przez każde sleep, a import ograniczany limitem szybkości może po cichu zagłodzić proces webowy. Jeśli podłączasz to do procesu publikacji, artykuł o webhookach dla zdarzeń linków opisuje pobieranie danych o kliknięciach bez odpytywania.

Którą opcję wybrać

Zadanie rake lub jednorazowy skrypt: Net::HTTP.post, cztery wiersze i gotowe. Cokolwiek zaplanowanego: wersja Net::HTTP.start z limitami czasu i kluczem idempotencji. Aplikacja Rails, która już używa Faraday: obiekt połączenia z ponowieniami zadeklarowanymi raz i używanymi wszędzie.

Wybór ma niewielkie znaczenie w porównaniu z trzema podstawowymi nawykami. Pobieraj klucz ze środowiska, ustaw jawny limit czasu i sprawdzaj klasę odpowiedzi przed sparsowaniem treści. Strona API i SDK zawiera listę wygenerowanych klientów, jeśli wolisz nie zarządzać tym samodzielnie, a rozwiązania dla deweloperów opisują pozostałe możliwości API po rozpoczęciu tworzenia linków z kodu.

Przeczytaj serię artykułów głównych

Ten artykuł należy do klastra engineering. Zacznij od przewodnika po bezpłatnym API skracacza URL, aby poznać kształt endpointu i uwierzytelnianie, a następnie przeczytaj limity szybkości i idempotencję, aby poprawnie zachowywać się pod obciążeniem. Aktualnym źródłem referencyjnym jest dokumentacja API, a skracacze URL dla deweloperów opisują, co sprawdzić w API przed rozpoczęciem pracy z nim.

Powiązane na blogu

Najczęściej zadawane pytania

Jak skrócić URL w Ruby?

Wyślij długi URL metodą POST do API skracacza za pomocą Net::HTTP, przekazując klucz API jako nagłówek Bearer i miejsce docelowe w treści JSON, a następnie odczytaj short_url z sparsowanej odpowiedzi. Net::HTTP.post to jednolinijkowe wywołanie w bibliotece standardowej, więc w pierwszej wersji niczego nie trzeba instalować.

Czy do skracania URL-i w Ruby potrzebuję gema?

Nie. Net::HTTP i biblioteka json są dostarczane z Ruby i obsługują całe wywołanie. Faraday ma sens, gdy potrzebujesz współużywalnego połączenia, middleware do ponowień i automatycznego kodowania JSON, co zwykle ma miejsce w aplikacji Rails, a nie w samodzielnym skrypcie.

Jak ustawić limit czasu w Net::HTTP?

Użyj Net::HTTP.start z open_timeout i read_timeout zamiast skrótu Net::HTTP.post, który nie pozwala ustawić żadnego z nich. Bez tych ustawień zawieszone połączenie może blokować workera przez pełne domyślne 60 sekund, a w zadaniu w tle oznacza to utratę jednego miejsca na zadanie.

Dlaczego Net::HTTP nie zgłasza wyjątku dla 401?

Ponieważ 401 to prawidłowa odpowiedź HTTP, a nie błąd transportu. Net::HTTP zgłasza wyjątki tylko dla błędów gniazda i limitów czasu, więc klasę odpowiedzi trzeba sprawdzić samodzielnie za pomocą is_a?(Net::HTTPSuccess) albo odczytać response.code. Middleware raise_error Faradaya robi to za Ciebie.

Jak skrócić wiele URL-i naraz w Ruby?

Umieść URL-e w Queue i uruchom przeciwko niej niewielką pulę wątków, zwykle około ośmiu. Ruby zwalnia globalną blokadę podczas operacji wejścia-wyjścia, więc wątki naprawdę nakładają oczekiwanie na sieć, a stała pula utrzymuje liczbę żądań poniżej limitu szybkości API, który nieograniczona pętla z jednym wątkiem na URL przekroczyłaby natychmiast.

Gdzie powinna znajdować się pętla ponowień w aplikacji Rails?

W zadaniu w tle, a nie w akcji kontrolera. Ponowienie, które śpi przez kilka sekund, przez cały ten czas zajmuje wątek Puma, więc partia ograniczana limitem szybkości może zagłodzić proces webowy. Active Job z adapterem kolejki obsługuje narastanie opóźnienia i bez dodatkowej pracy udostępnia historię ponowień.

Wypróbuj Elido

Wklej URL, otrzymaj krótki link

Bez rejestracji. Link działa 30 dni. Zarejestruj się, aby zachować go na zawsze.

Za darmo, bez rejestracji · 2 dziennie

Wypróbuj Elido

Skracarka URL hostowana w UE: własne domeny, głęboka analityka i otwarte API. Darmowy plan - bez karty kredytowej.

Tagi
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

Czytaj dalej