Page MenuHomePhorge

No OneTemporary

Size
11 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/tesla/middleware/follow_redirects.ex b/lib/tesla/middleware/follow_redirects.ex
index 0ac4ee2..d7af343 100644
--- a/lib/tesla/middleware/follow_redirects.ex
+++ b/lib/tesla/middleware/follow_redirects.ex
@@ -1,141 +1,78 @@
defmodule Tesla.Middleware.FollowRedirects do
@behaviour Tesla.Middleware
@moduledoc """
Follow 3xx redirects
### Example
```
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`)
- - `:force_redirect` - If the server response is 301 or 302, proceed with the redirect even
- if the original request was neither GET nor HEAD. Default is `false`
- - `:rewrite_method` - If the server responds is 301 or 302, rewrite the method to GET when
- performing the redirect. This will always set the body to nil.
- Default is `false`
- - `:preserve_headers` - Preserve the headers from the original request and send them along in the
- redirect. Default is `false`
"""
@max_redirects 5
- @force_redirect false
- @rewrite_method false
- @preserve_headers false
+ @redirect_statuses [301, 302, 303, 307, 308]
def call(env, next, opts \\ []) do
- opts =
- opts
- |> Keyword.put_new(:max_redirects, @max_redirects)
- |> Keyword.put_new(:force_redirect, @force_redirect)
- |> Keyword.put_new(:rewrite_method, @rewrite_method)
- |> Keyword.put_new(:preserve_headers, @preserve_headers)
+ max = Keyword.get(opts || [], :max_redirects, @max_redirects)
- # Initial value for remaining attempts
- rem = Keyword.fetch!(opts, :max_redirects)
-
- process_request(env, next, opts, rem)
+ redirect(env, next, max)
end
- # Status codes 301 and 302 were originally included in HTTP/1.0 and may be responded to
- # differently depending on the user client. Some clients will preserve the original request
- # method, whereas others will follow the redirect with a `GET`. This method attempts to follow
- # the original recommendaiton while allowing the user to override default behavior.
- defp process_response(%{status: status} = env, orig, next, opts, rem)
- when status in [301, 302] do
- method = Map.fetch!(env, :method)
- rewrite_method = Keyword.fetch!(opts, :rewrite_method)
- force_redirect = Keyword.fetch!(opts, :force_redirect)
-
- with {:ok, env} <- prepare_redirect(orig, env, opts) do
- cond do
- method in [:get, :head] and rewrite_method ->
- process_request(%{env | method: :get, body: nil}, next, opts, rem)
-
- method in [:get, :head] ->
- process_request(env, next, opts, rem)
-
- force_redirect and rewrite_method ->
- process_request(%{env | method: :get, body: nil}, next, opts, rem)
-
- force_redirect ->
- process_request(env, next, opts, rem)
-
- true ->
- {:ok, orig}
- end
- else
- {:error, {:no_location, env}} -> {:ok, env}
- end
- end
+ defp redirect(env, next, left) when left == 0 do
+ case Tesla.run(env, next) do
+ {:ok, %{status: status} = env} when not (status in @redirect_statuses) ->
+ {:ok, env}
- # Status code 303 is included in the HTTP/1.1 specification and always redirects with `GET`
- defp process_response(%{status: 303} = env, orig, next, opts, rem) do
- with {:ok, env} <- prepare_redirect(orig, env, opts) do
- process_request(%{env | method: :get, body: nil}, next, opts, rem)
- else
- {:error, {:no_location, env}} -> {:ok, env}
- end
- end
+ {:ok, _env} ->
+ {:error, {__MODULE__, :too_many_redirects}}
- # Status codes 307 and 308 always perform redirects without modifying the original method
- defp process_response(%{status: status} = env, orig, next, opts, rem)
- when status in [307, 308] do
- with {:ok, env} <- prepare_redirect(orig, env, opts) do
- process_request(env, next, opts, rem)
- else
- {:error, {:no_location, env}} -> {:ok, env}
+ error ->
+ error
end
end
- defp process_response(env, _, _, _, _), do: {:ok, env}
-
- defp process_request(env, next, opts, rem) when rem >= 0 do
- env
- |> Tesla.run(next)
- |> case do
- {:ok, resp} ->
- process_response(resp, env, next, opts, rem - 1)
-
- other ->
- other
- end
- end
-
- defp process_request(_, _, _, _) do
- {:error, {__MODULE__, :too_many_redirects}}
- 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}
- defp prepare_redirect(orig, env, opts) do
- case Tesla.get_header(env, "location") do
- nil ->
- {:error, {:no_location, env}}
+ location ->
+ location = parse_location(location, res)
- location ->
- env = %{orig | url: parse_location(location, env), query: []}
+ %{env | status: res.status}
+ |> new_request(location)
+ |> redirect(next, left - 1)
+ end
- env =
- if Keyword.fetch!(opts, :preserve_headers),
- do: env,
- else: %{env | headers: []}
-
- {:ok, env}
+ 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(%{status: 303} = env, new_location), do: %{env | url: new_location, method: :get}
+ defp new_request(env, new_location), do: %{env | url: new_location}
+
defp parse_location("/" <> _rest = location, env) do
env.url
|> URI.parse()
|> URI.merge(location)
|> URI.to_string()
end
defp parse_location(location, _env), do: location
end
diff --git a/test/tesla/middleware/follow_redirects_test.exs b/test/tesla/middleware/follow_redirects_test.exs
index 378bf88..6188dcb 100644
--- a/test/tesla/middleware/follow_redirects_test.exs
+++ b/test/tesla/middleware/follow_redirects_test.exs
@@ -1,110 +1,171 @@
defmodule Tesla.Middleware.FollowRedirectsTest do
use ExUnit.Case
defmodule Client do
use Tesla
plug Tesla.Middleware.FollowRedirects
adapter fn env ->
{status, headers, body} =
case env.url do
"http://example.com/0" ->
{200, [{"content-type", "text/plain"}], "foo bar"}
"http://example.com/" <> n ->
next = String.to_integer(n) - 1
{301, [{"location", "http://example.com/#{next}"}], ""}
end
{:ok, %{env | status: status, headers: headers, body: body}}
end
end
test "redirects if default max redirects isn't exceeded" do
assert {:ok, env} = Client.get("http://example.com/5")
assert env.status == 200
end
test "raise error when redirect default max redirects is exceeded" do
assert {:error, {Tesla.Middleware.FollowRedirects, :too_many_redirects}} ==
Client.get("http://example.com/6")
end
defmodule CustomMaxRedirectsClient do
use Tesla
plug Tesla.Middleware.FollowRedirects, max_redirects: 1
adapter fn env ->
{status, headers, body} =
case env.url do
"http://example.com/0" ->
{200, [{"content-type", "text/plain"}], "foo bar"}
"http://example.com/" <> n ->
next = String.to_integer(n) - 1
{301, [{"location", "http://example.com/#{next}"}], ""}
end
{:ok, %{env | status: status, headers: headers, body: body}}
end
end
alias CustomMaxRedirectsClient, as: CMRClient
test "redirects if custom max redirects isn't exceeded" do
assert {:ok, env} = CMRClient.get("http://example.com/1")
assert env.status == 200
end
test "raise error when custom max redirects is exceeded" do
assert {:error, {Tesla.Middleware.FollowRedirects, :too_many_redirects}} ==
CMRClient.get("http://example.com/2")
end
defmodule RelativeLocationClient do
use Tesla
plug Tesla.Middleware.FollowRedirects
adapter fn env ->
{status, headers, body} =
case env.url do
"https://example.com/pl" ->
{200, [{"content-type", "text/plain"}], "foo bar"}
"http://example.com" ->
{301, [{"location", "https://example.com"}], ""}
"https://example.com" ->
{301, [{"location", "/pl"}], ""}
"https://example.com/" ->
{301, [{"location", "/pl"}], ""}
"https://example.com/article" ->
{301, [{"location", "/pl"}], ""}
end
{:ok, %{env | status: status, headers: headers, body: body}}
end
end
alias RelativeLocationClient, as: RLClient
test "supports relative address in location header" do
assert {:ok, env} = RLClient.get("http://example.com")
assert env.status == 200
end
test "doesn't create double slashes inside new url" do
assert {:ok, env} = RLClient.get("https://example.com/")
assert env.url == "https://example.com/pl"
end
test "rewrites URLs to their root" do
assert {:ok, env} = RLClient.get("https://example.com/article")
assert env.url == "https://example.com/pl"
end
+
+ defmodule CustomRewriteMethodClient do
+ use Tesla
+
+ plug Tesla.Middleware.FollowRedirects
+
+ adapter fn env ->
+ {status, headers, body} =
+ case env.url do
+ "http://example.com/0" ->
+ {200, [{"content-type", "text/plain"}], "foo bar"}
+
+ "http://example.com/" <> n ->
+ next = String.to_integer(n) - 1
+ {303, [{"location", "http://example.com/#{next}"}], ""}
+ end
+
+ {:ok, %{env | status: status, headers: headers, body: body}}
+ end
+ end
+
+ alias CustomRewriteMethodClient, as: CRMClient
+
+ test "rewrites method to get for 303 requests" do
+ assert {:ok, env} = CRMClient.post("http://example.com/1", "")
+ assert env.method == :get
+ end
+
+ defmodule CustomPreservesRequestClient do
+ use Tesla
+
+ plug Tesla.Middleware.FollowRedirects
+
+ adapter fn env ->
+ {status, headers, body} =
+ case env.url do
+ "http://example.com/0" ->
+ {200, env.headers, env.body}
+
+ "http://example.com/" <> n ->
+ next = String.to_integer(n) - 1
+ {301, [{"location", "http://example.com/#{next}"}], ""}
+ end
+
+ {:ok, %{env | status: status, headers: headers, body: body}}
+ end
+ end
+
+ alias CustomPreservesRequestClient, as: CPRClient
+
+ test "Preserves original request" do
+ assert {:ok, env} =
+ CPRClient.post(
+ "http://example.com/1",
+ "Body data",
+ headers: [{"X-Custom-Header", "custom value"}]
+ )
+
+ assert env.body == "Body data"
+ assert env.headers == [{"X-Custom-Header", "custom value"}]
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 30, 8:22 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41403
Default Alt Text
(11 KB)

Event Timeline