From 692bd199bb15c69bd97c27fb076b62d0f4535081 Mon Sep 17 00:00:00 2001 From: Ian Warshak Date: Sat, 14 Jun 2025 08:57:37 -0500 Subject: [PATCH] added a request_retry_modifier function that can modify a request object before a retry --- lib/req.ex | 2 ++ lib/req/steps.ex | 21 +++++++++++++++++---- test/req/steps_test.exs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/req.ex b/lib/req.ex index b0a3388f..5c434c35 100644 --- a/lib/req.ex +++ b/lib/req.ex @@ -354,6 +354,8 @@ defmodule Req do * `{:delay, milliseconds}` - retry with the given delay. + * `{:delay, milliseconds, request_retry_modifier}` - retry with the given delay. The request object is passed to the request_retry_modifier function immediately before each retry. This can be helpful, for example, if you need to update an auth header that is based on the current time. + * `false/nil` - don't retry. * `false` - don't retry. diff --git a/lib/req/steps.ex b/lib/req/steps.ex index a1338816..41b28c28 100644 --- a/lib/req/steps.ex +++ b/lib/req/steps.ex @@ -1491,7 +1491,7 @@ defmodule Req.Steps do ## Options * `:raw` - if set to `true`, disables response body decompression. Defaults to `false`. - + Note: setting `raw: true` also disables response body decoding in the `decode_body/1` step. ## Examples @@ -2151,6 +2151,14 @@ defmodule Req.Steps do "expected :retry_delay not to be set when the :retry function is returning `{:delay, milliseconds}`" end + {:delay, delay, request_retry_modifier} when is_function(request_retry_modifier) -> + if !Req.Request.get_option(request, :retry_delay) do + retry(request, response_or_exception, delay, request_retry_modifier) + else + raise ArgumentError, + "expected :retry_delay not to be set when the :retry function is returning `{:delay, milliseconds, request_retry_modifier}`" + end + true -> retry(request, response_or_exception) @@ -2194,14 +2202,18 @@ defmodule Req.Steps 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) + do_retry(request, response_or_exception, &get_retry_delay/3, fn request -> request end) end defp retry(request, response_or_exception, delay) when is_integer(delay) do - do_retry(request, response_or_exception, fn request, _, _ -> {request, delay} end) + do_retry(request, response_or_exception, fn request, _, _ -> {request, delay} end, fn request -> request end) + end + + defp retry(request, response_or_exception, delay, request_retry_modifier) when is_integer(delay) and is_function(request_retry_modifier) do + do_retry(request, response_or_exception, fn request, _, _ -> {request, delay} end, request_retry_modifier) end - defp do_retry(request, response_or_exception, delay_getter) do + defp do_retry(request, response_or_exception, delay_getter, request_retry_modifier) when is_function(request_retry_modifier) do retry_count = Req.Request.get_private(request, :req_retry_count, 0) {request, delay} = delay_getter.(request, response_or_exception, retry_count) max_retries = Req.Request.get_option(request, :max_retries, 3) @@ -2210,6 +2222,7 @@ defmodule Req.Steps do if retry_count < max_retries do log_retry(response_or_exception, retry_count, max_retries, delay, log_level) Process.sleep(delay) + request = request_retry_modifier.(request) request = Req.Request.put_private(request, :req_retry_count, retry_count + 1) {request, response_or_exception} = Req.Request.run_request(%{request | halted: false}) Req.Request.halt(request, response_or_exception) diff --git a/test/req/steps_test.exs b/test/req/steps_test.exs index 9a758c76..326b53d7 100644 --- a/test/req/steps_test.exs +++ b/test/req/steps_test.exs @@ -1695,6 +1695,37 @@ defmodule Req.StepsTest do refute_received _ end + @tag :capture_log + test "custom function returning {:delay, milliseconds, request_modifier_fun}", c do + pid = self() + + Bypass.expect(c.bypass, "GET", "/", fn conn -> + case Plug.Conn.get_req_header(conn, "x-is-retry") do + [] -> send(pid, :ping) + ["true"] -> send(pid, :pong) + end + Plug.Conn.send_resp(conn, 500, "oops") + end) + + fun = fn _request, response -> + assert response.status == 500 + request_retry_modifier = fn request -> + request |> Req.Request.put_header("x-is-retry", "true") + end + {:delay, 1, request_retry_modifier} + end + + request = Req.new(url: c.url, retry: fun) + + assert Req.get!(request).status == 500 + assert_received :ping + assert_received :pong + assert_received :pong + assert_received :pong + refute_received _ + end + + @tag :capture_log test "raise on custom function returning {:delay, milliseconds} when `:retry_delay` is provided", c do