Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F116096
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
11 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 30, 8:22 AM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41403
Default Alt Text
(11 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment