lib/tesla/middleware/follow_redirects.ex

defmodule Tesla.Middleware.FollowRedirects do
  @moduledoc """
  Follow HTTP 3xx redirects.

  ## Examples

  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.FollowRedirects, max_redirects: 3 # defaults to 5
  end
  ```

  ## Options

  - `:max_redirects` - limit number of redirects (default: `5`)
  """

  @behaviour Tesla.Middleware

  @max_redirects 5
  @redirect_statuses [301, 302, 303, 307, 308]

  @impl Tesla.Middleware
  def call(env, next, opts \\ []) do
    max = Keyword.get(opts || [], :max_redirects, @max_redirects)

    redirect(env, next, max)
  end

  defp redirect(env, next, left) when left == 0 do
    case Tesla.run(env, next) do
      {:ok, %{status: status} = env} when status not in @redirect_statuses ->
        {:ok, env}

      {:ok, _env} ->
        {:error, {__MODULE__, :too_many_redirects}}

      error ->
        error
    end
  end

  defp redirect(env, next, left) do
    case Tesla.run(env, next) do
      {:ok, %{status: status} = res} when status in @redirect_statuses ->
        case Tesla.get_header(res, "location") do
          nil ->
            {:ok, res}

          location ->
            prev_uri = URI.parse(env.url)
            next_uri = parse_location(location, res)

            # Copy opts and query params from the response env,
            # these are not modified in the adapters, but middlewares
            # that come after might store state there
            env = %{env | opts: res.opts}

            env
            |> filter_headers(prev_uri, next_uri)
            |> new_request(status, URI.to_string(next_uri))
            |> redirect(next, left - 1)
        end

      other ->
        other
    end
  end

  # The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally
  # requested resource is not available, however a related resource (or another redirect)
  # available via GET is available at the specified location.
  # https://tools.ietf.org/html/rfc7231#section-6.4.4
  defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []}

  # The 307 (Temporary Redirect) status code indicates that the target
  # resource resides temporarily under a different URI and the user agent
  # MUST NOT change the request method (...)
  # https://tools.ietf.org/html/rfc7231#section-6.4.7
  defp new_request(env, 307, location), do: %{env | url: location}

  defp new_request(env, _, location), do: %{env | url: location, query: []}

  defp parse_location("https://" <> _rest = location, _env), do: URI.parse(location)
  defp parse_location("http://" <> _rest = location, _env), do: URI.parse(location)
  defp parse_location(location, env), do: env.url |> URI.parse() |> URI.merge(location)

  # See https://github.com/teamon/tesla/issues/362
  # See https://github.com/teamon/tesla/issues/360
  @filter_headers ["authorization", "host"]
  defp filter_headers(env, prev, next) do
    if next.host != prev.host || next.port != prev.port || next.scheme != prev.scheme do
      %{env | headers: Enum.filter(env.headers, fn {k, _} -> k not in @filter_headers end)}
    else
      env
    end
  end
end