From ad7dcf38a854ac762c812eae1ea5f8ba6b707cd6 Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Fri, 15 Dec 2023 17:12:45 +0000 Subject: [PATCH 1/8] Add HTTP backoff cache to respect 429s --- lib/pleroma/application.ex | 3 +- lib/pleroma/http/backoff.ex | 57 +++++++++++++++++++++++++++++++++++ lib/pleroma/object/fetcher.ex | 2 +- lib/pleroma/web/web_finger.ex | 5 +-- 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/http/backoff.ex diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 28a86d0aa..25fb11660 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -179,7 +179,8 @@ defp cachex_children do build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500), build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500), build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000), - build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300) + build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300), + build_cachex("http_backoff", default_ttl: :timer.hours(24 * 30), limit: 10000) ] end diff --git a/lib/pleroma/http/backoff.ex b/lib/pleroma/http/backoff.ex new file mode 100644 index 000000000..d51c0547a --- /dev/null +++ b/lib/pleroma/http/backoff.ex @@ -0,0 +1,57 @@ +defmodule Pleroma.HTTP.Backoff do + alias Pleroma.HTTP + require Logger + + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + @backoff_cache :http_backoff_cache + + defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do + # figure out from the 429 response when we can make the next request + # mastodon uses the x-ratelimit-reset header, so we will use that! + # other servers may not, so we'll default to 5 minutes from now if we can't find it + case Enum.find_value(headers, fn {"x-ratelimit-reset", value} -> value end) do + nil -> + DateTime.utc_now() + |> Timex.shift(seconds: 5 * 60) + + value -> + {:ok, stamp} = DateTime.from_iso8601(value) + stamp + end + end + + defp next_backoff_timestamp(_), do: DateTime.utc_now() |> Timex.shift(seconds: 5 * 60) + + def get(url, headers \\ [], options \\ []) do + # this acts as a single throughput for all GET requests + # we will check if the host is in the cache, and if it is, we will automatically fail the request + # this ensures that we don't hammer the server with requests, and instead wait for the backoff to expire + # this is a very simple implementation, and can be improved upon! + %{host: host} = URI.parse(url) + + case @cachex.get(@backoff_cache, host) do + {:ok, nil} -> + case HTTP.get(url, headers, options) do + {:ok, env} -> + case env.status do + 429 -> + Logger.error("Rate limited on #{host}! Backing off...") + timestamp = next_backoff_timestamp(env) + ttl = Timex.diff(timestamp, DateTime.utc_now(), :seconds) + # we will cache the host for 5 minutes + @cachex.put(@backoff_cache, host, true, ttl) + {:error, :ratelimit} + + _ -> + {:ok, env} + end + + {:error, env} -> + {:error, env} + end + + _ -> + {:error, :ratelimit} + end + end +end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index b9d8dbaaa..937026e04 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -354,7 +354,7 @@ def get_object(id) do with {:ok, %{body: body, status: code, headers: headers, url: final_url}} when code in 200..299 <- - HTTP.get(id, headers), + HTTP.Backoff.get(id, headers), remote_host <- URI.parse(final_url).host, {:cross_domain_redirect, false} <- diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index 9d5efbb3e..280ed236e 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -160,7 +160,8 @@ def find_lrdd_template(domain) do # WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1 meta_url = "https://#{domain}/.well-known/host-meta" - with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do + with {:ok, %{status: status, body: body}} when status in 200..299 <- + HTTP.Backoff.get(meta_url) do get_template_from_xml(body) else error -> @@ -197,7 +198,7 @@ def finger(account) do with address when is_binary(address) <- get_address_from_domain(domain, encoded_account), {:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <- - HTTP.get( + HTTP.Backoff.get( address, [{"accept", "application/xrd+xml,application/jrd+json"}] ) do From 2437a3e9ba191c20ef5231fa5a97bab99a8955a0 Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Fri, 15 Dec 2023 17:29:02 +0000 Subject: [PATCH 2/8] add test for backoff --- lib/pleroma/http/backoff.ex | 3 +-- test/pleroma/http/backoff_test.exs | 34 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 test/pleroma/http/backoff_test.exs diff --git a/lib/pleroma/http/backoff.ex b/lib/pleroma/http/backoff.ex index d51c0547a..d47d2ea6b 100644 --- a/lib/pleroma/http/backoff.ex +++ b/lib/pleroma/http/backoff.ex @@ -28,8 +28,7 @@ def get(url, headers \\ [], options \\ []) do # this ensures that we don't hammer the server with requests, and instead wait for the backoff to expire # this is a very simple implementation, and can be improved upon! %{host: host} = URI.parse(url) - - case @cachex.get(@backoff_cache, host) do + case @cachex.get(@backoff_cache, host) do {:ok, nil} -> case HTTP.get(url, headers, options) do {:ok, env} -> diff --git a/test/pleroma/http/backoff_test.exs b/test/pleroma/http/backoff_test.exs new file mode 100644 index 000000000..e8a571e87 --- /dev/null +++ b/test/pleroma/http/backoff_test.exs @@ -0,0 +1,34 @@ +defmodule Pleroma.HTTP.BackoffTest do + @backoff_cache :http_backoff_cache + use Pleroma.DataCase, async: false + alias Pleroma.HTTP.Backoff + + describe "get/3" do + test "should return {:ok, env} when not rate limited" do + Tesla.Mock.mock_global(fn + %Tesla.Env{url: "https://akkoma.dev/api/v1/instance"} -> + {:ok, %Tesla.Env{status: 200, body: "ok"}} + end) + assert {:ok, env} = Backoff.get("https://akkoma.dev/api/v1/instance") + assert env.status == 200 + end + + test "should return {:error, env} when rate limited" do + # Shove a value into the cache to simulate a rate limit + Cachex.put(@backoff_cache, "akkoma.dev", true) + assert {:error, env} = Backoff.get("https://akkoma.dev/api/v1/instance") + assert env.status == 429 + end + + test "should insert a value into the cache when rate limited" do + Tesla.Mock.mock_global(fn + %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> + {:ok, %Tesla.Env{status: 429, body: "Rate limited"}} + end) + + assert {:error, env} = Backoff.get("https://ratelimited.dev/api/v1/instance") + assert env.status == 429 + assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") + end + end +end From 3c384c1b7617f00a5222733e22e7b6cf7550c7fa Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Wed, 20 Dec 2023 16:45:35 +0000 Subject: [PATCH 3/8] Add ratelimit backoff to HTTP get --- lib/pleroma/http/backoff.ex | 21 ++++++++++++++++----- test/pleroma/http/backoff_test.exs | 22 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/http/backoff.ex b/lib/pleroma/http/backoff.ex index d47d2ea6b..cba6c0c17 100644 --- a/lib/pleroma/http/backoff.ex +++ b/lib/pleroma/http/backoff.ex @@ -9,14 +9,24 @@ defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do # figure out from the 429 response when we can make the next request # mastodon uses the x-ratelimit-reset header, so we will use that! # other servers may not, so we'll default to 5 minutes from now if we can't find it + default_5_minute_backoff = + DateTime.utc_now() + |> Timex.shift(seconds: 5 * 60) + case Enum.find_value(headers, fn {"x-ratelimit-reset", value} -> value end) do nil -> - DateTime.utc_now() - |> Timex.shift(seconds: 5 * 60) + Logger.error("Rate limited, but couldn't find timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}") + default_5_minute_backoff value -> - {:ok, stamp} = DateTime.from_iso8601(value) - stamp + with {:ok, stamp, _} <- DateTime.from_iso8601(value) do + Logger.error("Rate limited until #{stamp}") + stamp + else + _ -> + Logger.error("Rate limited, but couldn't parse timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}") + default_5_minute_backoff + end end end @@ -28,7 +38,8 @@ def get(url, headers \\ [], options \\ []) do # this ensures that we don't hammer the server with requests, and instead wait for the backoff to expire # this is a very simple implementation, and can be improved upon! %{host: host} = URI.parse(url) - case @cachex.get(@backoff_cache, host) do + + case @cachex.get(@backoff_cache, host) do {:ok, nil} -> case HTTP.get(url, headers, options) do {:ok, env} -> diff --git a/test/pleroma/http/backoff_test.exs b/test/pleroma/http/backoff_test.exs index e8a571e87..b50a4c458 100644 --- a/test/pleroma/http/backoff_test.exs +++ b/test/pleroma/http/backoff_test.exs @@ -9,6 +9,7 @@ test "should return {:ok, env} when not rate limited" do %Tesla.Env{url: "https://akkoma.dev/api/v1/instance"} -> {:ok, %Tesla.Env{status: 200, body: "ok"}} end) + assert {:ok, env} = Backoff.get("https://akkoma.dev/api/v1/instance") assert env.status == 200 end @@ -29,6 +30,25 @@ test "should insert a value into the cache when rate limited" do assert {:error, env} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert env.status == 429 assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") - end + end + + test "should parse the value of x-ratelimit-reset, if present" do + ten_minutes_from_now = + DateTime.utc_now() |> Timex.shift(minutes: 10) |> DateTime.to_iso8601() + + Tesla.Mock.mock_global(fn + %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> + {:ok, + %Tesla.Env{ + status: 429, + body: "Rate limited", + headers: [{"x-ratelimit-reset", ten_minutes_from_now}] + }} + end) + + assert {:error, env} = Backoff.get("https://ratelimited.dev/api/v1/instance") + assert env.status == 429 + assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") + end end end From ec7e9da734590622d180cb72dfc34ba3ab6bcfea Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Fri, 26 Apr 2024 19:05:12 +0100 Subject: [PATCH 4/8] Correct ttl syntax for new cachex --- lib/pleroma/http/backoff.ex | 2 +- test/pleroma/http/backoff_test.exs | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/http/backoff.ex b/lib/pleroma/http/backoff.ex index cba6c0c17..5fb37205e 100644 --- a/lib/pleroma/http/backoff.ex +++ b/lib/pleroma/http/backoff.ex @@ -49,7 +49,7 @@ def get(url, headers \\ [], options \\ []) do timestamp = next_backoff_timestamp(env) ttl = Timex.diff(timestamp, DateTime.utc_now(), :seconds) # we will cache the host for 5 minutes - @cachex.put(@backoff_cache, host, true, ttl) + @cachex.put(@backoff_cache, host, true, ttl: ttl) {:error, :ratelimit} _ -> diff --git a/test/pleroma/http/backoff_test.exs b/test/pleroma/http/backoff_test.exs index b50a4c458..62419eb5c 100644 --- a/test/pleroma/http/backoff_test.exs +++ b/test/pleroma/http/backoff_test.exs @@ -17,8 +17,7 @@ test "should return {:ok, env} when not rate limited" do test "should return {:error, env} when rate limited" do # Shove a value into the cache to simulate a rate limit Cachex.put(@backoff_cache, "akkoma.dev", true) - assert {:error, env} = Backoff.get("https://akkoma.dev/api/v1/instance") - assert env.status == 429 + assert {:error, :ratelimit} = Backoff.get("https://akkoma.dev/api/v1/instance") end test "should insert a value into the cache when rate limited" do @@ -27,8 +26,7 @@ test "should insert a value into the cache when rate limited" do {:ok, %Tesla.Env{status: 429, body: "Rate limited"}} end) - assert {:error, env} = Backoff.get("https://ratelimited.dev/api/v1/instance") - assert env.status == 429 + assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") end @@ -46,8 +44,7 @@ test "should parse the value of x-ratelimit-reset, if present" do }} end) - assert {:error, env} = Backoff.get("https://ratelimited.dev/api/v1/instance") - assert env.status == 429 + assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") end end From 9671cdecdf9a8bf968a7b1a87091b57c9490924a Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Fri, 26 Apr 2024 19:10:17 +0100 Subject: [PATCH 5/8] changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2facbd84d..257dc4adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Issue leading to Mastodon bot accounts being rejected - Scope misdetection of remote posts resulting from not recognising JSON-LD-compacted forms of public scope; affected e.g. federation with bovine +- Ratelimits encountered when fetching objects are now respected; 429 responses will cause a backoff when we get one. ## Removed - ActivityPub Client-To-Server write API endpoints have been disabled; From 010e8c7bb26d537acf2b1d969adcc6ba42abe9a9 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Fri, 26 Apr 2024 19:28:01 +0100 Subject: [PATCH 6/8] where were you when lint fail --- lib/pleroma/http/backoff.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/http/backoff.ex b/lib/pleroma/http/backoff.ex index 5fb37205e..dac05c971 100644 --- a/lib/pleroma/http/backoff.ex +++ b/lib/pleroma/http/backoff.ex @@ -15,7 +15,10 @@ defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do case Enum.find_value(headers, fn {"x-ratelimit-reset", value} -> value end) do nil -> - Logger.error("Rate limited, but couldn't find timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}") + Logger.error( + "Rate limited, but couldn't find timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}" + ) + default_5_minute_backoff value -> @@ -24,7 +27,10 @@ defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do stamp else _ -> - Logger.error("Rate limited, but couldn't parse timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}") + Logger.error( + "Rate limited, but couldn't parse timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}" + ) + default_5_minute_backoff end end From bd74693db63925a4469c618989dcc7c05aa81591 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Mon, 6 May 2024 23:34:48 +0100 Subject: [PATCH 7/8] additionally support retry-after values --- lib/pleroma/http/backoff.ex | 136 +++++++++++++++++++---------- test/pleroma/http/backoff_test.exs | 44 ++++++++++ 2 files changed, 136 insertions(+), 44 deletions(-) diff --git a/lib/pleroma/http/backoff.ex b/lib/pleroma/http/backoff.ex index dac05c971..b3f734a92 100644 --- a/lib/pleroma/http/backoff.ex +++ b/lib/pleroma/http/backoff.ex @@ -5,66 +5,114 @@ defmodule Pleroma.HTTP.Backoff do @cachex Pleroma.Config.get([:cachex, :provider], Cachex) @backoff_cache :http_backoff_cache + # attempt to parse a timestamp from a header + # returns nil if it can't parse the timestamp + @spec timestamp_or_nil(binary) :: DateTime.t() | nil + defp timestamp_or_nil(header) do + case DateTime.from_iso8601(header) do + {:ok, stamp, _} -> + stamp + + _ -> + nil + end + end + + # attempt to parse the x-ratelimit-reset header from the headers + @spec x_ratelimit_reset(headers :: list) :: DateTime.t() | nil + defp x_ratelimit_reset(headers) do + with {_header, value} <- List.keyfind(headers, "x-ratelimit-reset", 0), + true <- is_binary(value) do + timestamp_or_nil(value) + else + _ -> + nil + end + end + + # attempt to parse the Retry-After header from the headers + # this can be either a timestamp _or_ a number of seconds to wait! + # we'll return a datetime if we can parse it, or nil if we can't + @spec retry_after(headers :: list) :: DateTime.t() | nil + defp retry_after(headers) do + with {_header, value} <- List.keyfind(headers, "retry-after", 0), + true <- is_binary(value) do + # first, see if it's an integer + case Integer.parse(value) do + {seconds, ""} -> + Logger.debug("Parsed Retry-After header: #{seconds} seconds") + DateTime.utc_now() |> Timex.shift(seconds: seconds) + + _ -> + # if it's not an integer, try to parse it as a timestamp + timestamp_or_nil(value) + end + else + _ -> + nil + end + end + + # given a set of headers, will attempt to find the next backoff timestamp + # if it can't find one, it will default to 5 minutes from now + @spec next_backoff_timestamp(%{headers: list}) :: DateTime.t() defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do - # figure out from the 429 response when we can make the next request - # mastodon uses the x-ratelimit-reset header, so we will use that! - # other servers may not, so we'll default to 5 minutes from now if we can't find it default_5_minute_backoff = DateTime.utc_now() |> Timex.shift(seconds: 5 * 60) - case Enum.find_value(headers, fn {"x-ratelimit-reset", value} -> value end) do - nil -> - Logger.error( - "Rate limited, but couldn't find timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}" - ) + backoff = + [&x_ratelimit_reset/1, &retry_after/1] + |> Enum.map(& &1.(headers)) + |> Enum.find(&(&1 != nil)) - default_5_minute_backoff - - value -> - with {:ok, stamp, _} <- DateTime.from_iso8601(value) do - Logger.error("Rate limited until #{stamp}") - stamp - else - _ -> - Logger.error( - "Rate limited, but couldn't parse timestamp! Using default 5 minute backoff until #{default_5_minute_backoff}" - ) - - default_5_minute_backoff - end + if is_nil(backoff) do + Logger.debug("No backoff headers found, defaulting to 5 minutes from now") + default_5_minute_backoff + else + Logger.debug("Found backoff header, will back off until: #{backoff}") + backoff end end defp next_backoff_timestamp(_), do: DateTime.utc_now() |> Timex.shift(seconds: 5 * 60) + # utility function to check the HTTP response for potential backoff headers + # will check if we get a 429 or 503 response, and if we do, will back off for a bit + @spec check_backoff({:ok | :error, HTTP.Env.t()}, binary()) :: + {:ok | :error, HTTP.Env.t()} | {:error, :ratelimit} + defp check_backoff({:ok, env}, host) do + case env.status do + status when status in [429, 503] -> + Logger.error("Rate limited on #{host}! Backing off...") + timestamp = next_backoff_timestamp(env) + ttl = Timex.diff(timestamp, DateTime.utc_now(), :seconds) + # we will cache the host for 5 minutes + @cachex.put(@backoff_cache, host, true, ttl: ttl) + {:error, :ratelimit} + + _ -> + {:ok, env} + end + end + + defp check_backoff(env, _), do: env + + @doc """ + this acts as a single throughput for all GET requests + we will check if the host is in the cache, and if it is, we will automatically fail the request + this ensures that we don't hammer the server with requests, and instead wait for the backoff to expire + this is a very simple implementation, and can be improved upon! + """ + @spec get(binary, list, list) :: {:ok | :error, HTTP.Env.t()} | {:error, :ratelimit} def get(url, headers \\ [], options \\ []) do - # this acts as a single throughput for all GET requests - # we will check if the host is in the cache, and if it is, we will automatically fail the request - # this ensures that we don't hammer the server with requests, and instead wait for the backoff to expire - # this is a very simple implementation, and can be improved upon! %{host: host} = URI.parse(url) case @cachex.get(@backoff_cache, host) do {:ok, nil} -> - case HTTP.get(url, headers, options) do - {:ok, env} -> - case env.status do - 429 -> - Logger.error("Rate limited on #{host}! Backing off...") - timestamp = next_backoff_timestamp(env) - ttl = Timex.diff(timestamp, DateTime.utc_now(), :seconds) - # we will cache the host for 5 minutes - @cachex.put(@backoff_cache, host, true, ttl: ttl) - {:error, :ratelimit} - - _ -> - {:ok, env} - end - - {:error, env} -> - {:error, env} - end + url + |> HTTP.get(headers, options) + |> check_backoff(host) _ -> {:error, :ratelimit} diff --git a/test/pleroma/http/backoff_test.exs b/test/pleroma/http/backoff_test.exs index 62419eb5c..33a4fd22f 100644 --- a/test/pleroma/http/backoff_test.exs +++ b/test/pleroma/http/backoff_test.exs @@ -3,6 +3,10 @@ defmodule Pleroma.HTTP.BackoffTest do use Pleroma.DataCase, async: false alias Pleroma.HTTP.Backoff + defp within_tolerance?(ttl, expected) do + ttl > expected - 10 and ttl < expected + 10 + end + describe "get/3" do test "should return {:ok, env} when not rate limited" do Tesla.Mock.mock_global(fn @@ -46,6 +50,46 @@ test "should parse the value of x-ratelimit-reset, if present" do assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") + {:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev") + assert within_tolerance?(ttl, 600) + end + + test "should parse the value of retry-after when it's a timestamp" do + ten_minutes_from_now = + DateTime.utc_now() |> Timex.shift(minutes: 10) |> DateTime.to_iso8601() + + Tesla.Mock.mock_global(fn + %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> + {:ok, + %Tesla.Env{ + status: 429, + body: "Rate limited", + headers: [{"retry-after", ten_minutes_from_now}] + }} + end) + + assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") + assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") + {:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev") + assert within_tolerance?(ttl, 600) + end + + test "should parse the value of retry-after when it's a number of seconds" do + Tesla.Mock.mock_global(fn + %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> + {:ok, + %Tesla.Env{ + status: 429, + body: "Rate limited", + headers: [{"retry-after", "600"}] + }} + end) + + assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") + assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") + # assert that the value is 10 minutes from now + {:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev") + assert within_tolerance?(ttl, 600) end end end From ea6bc8a7c587c636793875c4d8d7ed534288336e Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Mon, 6 May 2024 23:36:00 +0100 Subject: [PATCH 8/8] add a test for 503-rate-limiting --- test/pleroma/http/backoff_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/pleroma/http/backoff_test.exs b/test/pleroma/http/backoff_test.exs index 33a4fd22f..f1b27f5b5 100644 --- a/test/pleroma/http/backoff_test.exs +++ b/test/pleroma/http/backoff_test.exs @@ -34,6 +34,16 @@ test "should insert a value into the cache when rate limited" do assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") end + test "should insert a value into the cache when rate limited with a 503 response" do + Tesla.Mock.mock_global(fn + %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> + {:ok, %Tesla.Env{status: 503, body: "Rate limited"}} + end) + + assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") + assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") + end + test "should parse the value of x-ratelimit-reset, if present" do ten_minutes_from_now = DateTime.utc_now() |> Timex.shift(minutes: 10) |> DateTime.to_iso8601()