Req: Accumulating redirects - is this the proper way to go about it?

Dear all,

I threw together a custom step for req that accumulates the responses from the redirects that were followed.

Basically, I re-implemented the follow_redirects step from req and added accumulation which is then stored to the “private part” of the response.

Would you agree that this is the proper way to go about achieving this result?

The step

defmodule MyApp.FollowStoreRedirects do
  require Logger

  def attach(%Req.Request{} = request) do
    request
    |> Req.Request.prepend_response_steps(follow_redirects: &follow_store_redirects/1)
  end

  @doc step: :response
  defp follow_store_redirects(request_response)

  defp follow_store_redirects({request, response})
       when request.options.follow_redirects == false do
    {request, response}
  end

  defp follow_store_redirects({request, %{status: status} = response})
       when status in [301, 302, 303, 307, 308] do
    max_redirects = Map.get(request.options, :max_redirects, 10)
    redirect_count = Req.Request.get_private(request, :req_redirect_count, 0)
    stored_redirects = Req.Request.get_private(request, :stored_redirects, [])

    if redirect_count < max_redirects do
      request =
        request
        |> build_redirect_request(response)
        |> Req.Request.put_private(:req_redirect_count, redirect_count + 1)
        |> Req.Request.put_private(:stored_redirects, [response | stored_redirects])

      {_, result} = Req.Request.run(request)

      {Req.Request.halt(request), result}
    else
      raise "too many redirects (#{max_redirects})"
    end
  end

  defp follow_store_redirects({request, response}) do
    stored_redirects = Req.Request.get_private(request, :stored_redirects, [])

    {request,
     response
     |> Req.Response.put_private(:stored_redirects, stored_redirects)}
  end

  defp build_redirect_request(request, response) do
    {_, location} = List.keyfind(response.headers, "location", 0)
    Logger.debug(["follow_redirects: redirecting to : ", location])

    location_trusted = Map.get(request.options, :location_trusted)

    location_url = URI.merge(request.url, URI.parse(location))

    request
    |> remove_params()
    |> remove_credentials_if_untrusted(location_trusted, location_url)
    |> put_redirect_request_method()
    |> put_redirect_location(location_url)
  end

  defp put_redirect_request_method(request) when request.status in 307..308, do: request

  defp put_redirect_request_method(request), do: %{request | method: :get}

  defp remove_credentials_if_untrusted(request, true, _), do: request

  defp remove_credentials_if_untrusted(request, _, location_url) do
    if {location_url.host, location_url.scheme, location_url.port} ==
         {request.url.host, request.url.scheme, request.url.port} do
      request
    else
      remove_credentials(request)
    end
  end

  defp remove_credentials(request) do
    headers = List.keydelete(request.headers, "authorization", 0)
    request = update_in(request.options, &Map.delete(&1, :auth))
    %{request | headers: headers}
  end

  defp put_redirect_location(request, location_url) do
    put_in(request.url, location_url)
  end

  defp remove_params(request) do
    update_in(request.options, &Map.delete(&1, :params))
  end
end

Using the step

req =
  Req.new()
  |> MyApp.FollowStoreRedirects.attach()

Req.get!(req, url: "https://httpbin.org/redirect/5", max_redirects: 5)

Thank you in advance for any response and have a great rest of your day!

2 Likes

Instead of re-implementing follow_redirects I think you could prepend a custom step beforehand which upon getting 3xx response would save it off but then let follow_redirects do the actual redirect. Would that work?

1 Like

Many thanks Wojtek!

*obviously that’s how to do it and I feel stupid right now

**was super pressed for time and wanted to explore it really quickly, not particularly wisely however

1 Like