Skip to content

Commit

Permalink
switch to :retry 2-arity fun, add support for {:delay, milliseconds} (
Browse files Browse the repository at this point in the history
  • Loading branch information
DaTrader authored Aug 25, 2023
1 parent 1947528 commit 7b0286f
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 101 deletions.
57 changes: 56 additions & 1 deletion lib/req/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ defmodule Req.Response do
@doc """
Deletes the header given by `key`
All occurences of the header are delete, in case the header is repeated multiple times.
All occurences of the header are deleted, in case the header is repeated multiple times.
## Examples
Expand All @@ -167,4 +167,59 @@ defmodule Req.Response do
)
}
end

@doc """
Returns the `retry-after` header delay value or nil if not found.
"""
@spec get_retry_after(t()) :: integer() | nil
def get_retry_after(response) do
case get_header(response, "retry-after") do
[delay] ->
retry_delay_in_ms(delay)

[] ->
nil
end
end

defp retry_delay_in_ms(delay_value) do
case Integer.parse(delay_value) do
{seconds, ""} ->
:timer.seconds(seconds)

:error ->
delay_value
|> parse_http_datetime()
|> DateTime.diff(DateTime.utc_now(), :millisecond)
|> max(0)
end
end

@month_numbers %{
"Jan" => "01",
"Feb" => "02",
"Mar" => "03",
"Apr" => "04",
"May" => "05",
"Jun" => "06",
"Jul" => "07",
"Aug" => "08",
"Sep" => "09",
"Oct" => "10",
"Nov" => "11",
"Dec" => "12"
}

defp parse_http_datetime(datetime) do
[_day_of_week, day, month, year, time, "GMT"] = String.split(datetime, " ")
date = year <> "-" <> @month_numbers[month] <> "-" <> day

case DateTime.from_iso8601(date <> " " <> time <> "Z") do
{:ok, valid_datetime, 0} ->
valid_datetime

{:error, reason} ->
raise "cannot parse \"retry-after\" header value #{inspect(datetime)} as datetime, reason: #{reason}"
end
end
end
162 changes: 74 additions & 88 deletions lib/req/steps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -862,9 +862,7 @@ defmodule Req.Steps do
| _other_ | Returns data as is |
This step updates the following headers to reflect the changes:
* `content-length` is set to the length of the decompressed body
* `content-encoding` is removed
- `content-length` is set to the length of the decompressed body
## Options
Expand Down Expand Up @@ -906,59 +904,49 @@ defmodule Req.Steps do
if request.options[:raw] do
{request, response}
else
codecs = get_content_encoding_header(response.headers)
{decompressed_body, unknown_codecs} = decompress_body(codecs, response.body, [])
compression_algorithms = get_content_encoding_header(response.headers)
decompressed_body = decompress_body(response.body, compression_algorithms)
decompressed_content_length = decompressed_body |> byte_size() |> to_string()

response =
%Req.Response{response | body: decompressed_body}
|> Req.Response.put_header("content-length", decompressed_content_length)

response =
if unknown_codecs == [] do
Req.Response.delete_header(response, "content-encoding")
else
Req.Response.put_header(response, "content-encoding", Enum.join(unknown_codecs, ", "))
end

{request, response}
end
end

defp decompress_body([gzip | rest], body, acc) when gzip in ["gzip", "x-gzip"] do
decompress_body(rest, :zlib.gunzip(body), acc)
defp decompress_body(body, algorithms) do
Enum.reduce(algorithms, body, &decompress_with_algorithm(&1, &2))
end

defp decompress_body(["br" | rest], body, acc) do
defp decompress_with_algorithm(gzip, body) when gzip in ["gzip", "x-gzip"] do
:zlib.gunzip(body)
end

defp decompress_with_algorithm("br", body) do
if brotli_loaded?() do
{:ok, decompressed} = :brotli.decode(body)
decompress_body(rest, decompressed, acc)
decompressed
else
Logger.debug("decompress_body: :brotli library not loaded, skipping brotli decompression")
decompress_body(rest, body, ["br" | acc])
raise("`:brotli` decompression library not loaded")
end
end

defp decompress_body(["zstd" | rest], body, acc) do
defp decompress_with_algorithm("zstd", body) do
if ezstd_loaded?() do
decompress_body(rest, :ezstd.decompress(body), acc)
:ezstd.decompress(body)
else
Logger.debug("decompress_body: :ezstd library not loaded, skipping zstd decompression")
decompress_body(rest, body, ["zstd" | acc])
raise("`:ezstd` decompression library not loaded")
end
end

defp decompress_body(["identity" | rest], body, acc) do
decompress_body(rest, body, acc)
end

defp decompress_body([codec | rest], body, acc) do
Logger.debug("decompress_body: algorithm #{inspect(codec)} is not supported")
decompress_body(rest, body, [codec | acc])
defp decompress_with_algorithm("identity", body) do
body
end

defp decompress_body([], body, acc) do
{body, acc}
defp decompress_with_algorithm(_algorithm, body) do
body
end

defmacrop nimble_csv_loaded? do
Expand Down Expand Up @@ -1341,8 +1329,14 @@ defmodule Req.Steps do
* `:transient` - same as `:safe_transient` except retries all HTTP methods (POST, DELETE, etc.)
* `fun` - a 1-arity function that accepts either a `Req.Response` or an exception struct
and returns boolean whether to retry.
* `fun` - a 2-arity function that accepts a `Req.Request` and either a `Req.Response` or an exception struct
and returns one of the following:
* `true` if to retry according to the `:retry_delay` option logic (see below)
* `{:delay, milliseconds}` to retry after the delay in milliseconds
* `false` or `nil` if not to retry
* `false` - never retry.
Expand All @@ -1353,6 +1347,9 @@ defmodule Req.Steps do
If the response is HTTP 429 and contains the `retry-after` header, the value of the header is used to
determine the next retry delay.
This option must not be provided if a `:retry` function returning `{:delay, milliseconds}` is, or an
exception will be raised otherwise.
* `:retry_log_level` - the log level to emit retry logs at. Can also be set to `false` to disable
logging these messsages. Defaults to `:error`.
Expand Down Expand Up @@ -1381,7 +1378,7 @@ defmodule Req.Steps do
def retry(request_response_or_error)

def retry({request, response_or_exception}) do
retry? =
retry =
case Map.get(request.options, :retry, :safe_transient) do
:safe_transient ->
safe_transient?(request, response_or_exception)
Expand All @@ -1393,7 +1390,7 @@ defmodule Req.Steps do
false

fun when is_function(fun) ->
fun.(response_or_exception)
apply_retry(fun, request, response_or_exception)

:safe ->
IO.warn("setting `retry: :safe` is deprecated in favour of `retry: :safe_transient`")
Expand All @@ -1405,17 +1402,38 @@ defmodule Req.Steps do

other ->
raise ArgumentError,
"expected :retry to be :safe_transient, :transient, false, or a 1-arity function, " <>
"expected :retry to be :safe_transient, :transient, false, or a 2-arity function, " <>
"got: #{inspect(other)}"
end

if retry? do
retry(request, response_or_exception)
else
{request, response_or_exception}
case retry do
{:delay, delay} ->
if !Req.Request.get_option(request, :retry_delay) do
retry(request, response_or_exception, delay)
else
raise ArgumentError,
"expected :retry_delay not to be set when the :retry function is returning `{:delay, milliseconds}`"
end

true ->
retry(request, response_or_exception)

retry when retry in [false, nil] ->
{request, response_or_exception}
end
end

defp apply_retry( fun, request, response_or_exception)

defp apply_retry(fun, _request, response_or_exception) when is_function( fun, 1) do
IO.warn("`retry: fun/1` has been deprecated in favor of `retry: fun/2`")
fun.(response_or_exception)
end

defp apply_retry(fun, request, response_or_exception) when is_function( fun, 2) do
fun.(request, response_or_exception)
end

defp safe_transient?(request, response_or_exception) do
request.method in [:get, :head] and transient?(response_or_exception)
end
Expand All @@ -1437,9 +1455,19 @@ defmodule Req.Steps do
false
end

defp retry(request, response_or_exception) do
defp retry(request, response_or_exception, delay_or_nil \\ nil)

defp retry(request, response_or_exception, nil) do
do_retry(request, response_or_exception, &get_retry_delay/3)
end

defp retry(request, response_or_exception, delay) when is_integer(delay) do
do_retry(request, response_or_exception, fn request, _, _ -> {request, delay} end)
end

defp do_retry(request, response_or_exception, delay_getter) do
retry_count = Req.Request.get_private(request, :req_retry_count, 0)
{request, delay} = get_retry_delay(request, response_or_exception, retry_count)
{request, delay} = delay_getter.(request, response_or_exception, retry_count)
max_retries = Req.Request.get_option(request, :max_retries, 3)
log_level = Req.Request.get_option(request, :retry_log_level, :error)

Expand All @@ -1455,12 +1483,10 @@ defmodule Req.Steps do
end

defp get_retry_delay(request, %Req.Response{status: 429} = response, retry_count) do
case Req.Response.get_header(response, "retry-after") do
[delay] ->
{request, retry_delay_in_ms(delay)}

[] ->
calculate_retry_delay(request, retry_count)
if delay = Req.Response.get_retry_after(response) do
{request, delay}
else
calculate_retry_delay(request, retry_count)
end
end

Expand All @@ -1482,19 +1508,6 @@ defmodule Req.Steps do
Integer.pow(2, n) * 1000
end

defp retry_delay_in_ms(delay_value) do
case Integer.parse(delay_value) do
{seconds, ""} ->
:timer.seconds(seconds)

:error ->
delay_value
|> parse_http_datetime()
|> DateTime.diff(DateTime.utc_now(), :millisecond)
|> max(0)
end
end

defp log_retry(_, _, _, _, false), do: :ok

defp log_retry(response_or_exception, retry_count, max_retries, delay, level) do
Expand Down Expand Up @@ -1568,31 +1581,4 @@ defmodule Req.Steps do
def format_http_datetime(datetime) do
Calendar.strftime(datetime, "%a, %d %b %Y %H:%M:%S GMT")
end

@month_numbers %{
"Jan" => "01",
"Feb" => "02",
"Mar" => "03",
"Apr" => "04",
"May" => "05",
"Jun" => "06",
"Jul" => "07",
"Aug" => "08",
"Sep" => "09",
"Oct" => "10",
"Nov" => "11",
"Dec" => "12"
}
defp parse_http_datetime(datetime) do
[_day_of_week, day, month, year, time, "GMT"] = String.split(datetime, " ")
date = year <> "-" <> @month_numbers[month] <> "-" <> day

case DateTime.from_iso8601(date <> " " <> time <> "Z") do
{:ok, valid_datetime, 0} ->
valid_datetime

{:error, reason} ->
raise "could not parse \"Retry-After\" header #{datetime} - #{reason}"
end
end
end
Loading

0 comments on commit 7b0286f

Please sign in to comment.