diff --git a/lib/httpoison.ex b/lib/httpoison.ex index fd1ac3b..2160ecf 100644 --- a/lib/httpoison.ex +++ b/lib/httpoison.ex @@ -26,7 +26,8 @@ defmodule HTTPoison.Request do * `:socks5_user`- socks5 username * `:socks5_pass`- socks5 password * `:ssl` - SSL options supported by the `ssl` erlang module - * `:follow_redirect` - a boolean that causes redirects to be followed + * `:follow_redirect` - a boolean that causes redirects to be followed, can cause a request to return + a `MaybeRedirect` struct. See: HTTPoison.MaybeRedirect * `:max_redirect` - an integer denoting the maximum number of redirects to follow. Default is 5 * `:params` - an enumerable consisting of two-item tuples that will be appended to the url as query string parameters * `:max_body_length` - a non-negative integer denoting the max response body length. See :hackney.body/2 @@ -95,6 +96,30 @@ defmodule HTTPoison.AsyncEnd do @type t :: %__MODULE__{id: reference} end +defmodule HTTPoison.MaybeRedirect do + @moduledoc """ + If the option `:follow_redirect` is given to a request, HTTP redirects are automatically follow if + the method is set to `:get` or `:head` and the response's `status_code` is `301`, `302` or `307`. + + If the method is set to `:post`, then the only `status_code` that get's automatically + followed is `303`. + + If any other method or `status_code` is returned, then this struct is returned in place of a + `HTTPoison.Response` or `HTTPoison.AsyncResponse`, containing the `redirect_url` to allow you + to optionally re-request with the method set to `:get`. + """ + + defstruct status_code: nil, request_url: nil, request: nil, redirect_url: nil, headers: [] + + @type t :: %__MODULE__{ + status_code: integer, + headers: list, + request: HTTPoison.Request.t(), + request_url: HTTPoison.Request.url(), + redirect_url: HTTPoison.Request.url() + } +end + defmodule HTTPoison.Error do defexception reason: nil, id: nil @type t :: %__MODULE__{id: reference | nil, reason: any} diff --git a/lib/httpoison/base.ex b/lib/httpoison/base.ex index 0ce6447..745c8a1 100644 --- a/lib/httpoison/base.ex +++ b/lib/httpoison/base.ex @@ -75,19 +75,22 @@ defmodule HTTPoison.Base do alias HTTPoison.Request alias HTTPoison.Response alias HTTPoison.AsyncResponse + alias HTTPoison.MaybeRedirect alias HTTPoison.Error - @callback delete(url) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} - @callback delete(url, headers) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + @callback delete(url) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} + @callback delete(url, headers) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback delete(url, headers, options) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} - @callback delete!(url) :: Response.t() | AsyncResponse.t() - @callback delete!(url, headers) :: Response.t() | AsyncResponse.t() - @callback delete!(url, headers, options) :: Response.t() | AsyncResponse.t() + @callback delete!(url) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback delete!(url, headers) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback delete!(url, headers, options) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() @callback get(url) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} - @callback get(url, headers) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + @callback get(url, headers) :: {:ok, Response.t() | AsyncResponse.t() | {:error, Error.t()}} @callback get(url, headers, options) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} @@ -104,34 +107,41 @@ defmodule HTTPoison.Base do @callback head!(url, headers) :: Response.t() | AsyncResponse.t() @callback head!(url, headers, options) :: Response.t() | AsyncResponse.t() - @callback options(url) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} - @callback options(url, headers) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + @callback options(url) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} + @callback options(url, headers) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback options(url, headers, options) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} - @callback options!(url) :: Response.t() | AsyncResponse.t() - @callback options!(url, headers) :: Response.t() | AsyncResponse.t() - @callback options!(url, headers, options) :: Response.t() | AsyncResponse.t() + @callback options!(url) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback options!(url, headers) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback options!(url, headers, options) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() - @callback patch(url, body) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + @callback patch(url, body) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback patch(url, body, headers) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback patch(url, body, headers, options) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} - @callback patch!(url, body) :: Response.t() | AsyncResponse.t() - @callback patch!(url, body, headers) :: Response.t() | AsyncResponse.t() - @callback patch!(url, body, headers, options) :: Response.t() | AsyncResponse.t() + @callback patch!(url, body) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback patch!(url, body, headers) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback patch!(url, body, headers, options) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() - @callback post(url, body) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + @callback post(url, body) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback post(url, body, headers) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback post(url, body, headers, options) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} - @callback post!(url, body) :: Response.t() | AsyncResponse.t() - @callback post!(url, body, headers) :: Response.t() | AsyncResponse.t() - @callback post!(url, body, headers, options) :: Response.t() | AsyncResponse.t() + @callback post!(url, body) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback post!(url, body, headers) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback post!(url, body, headers, options) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() # deprecated: Use process_request_headers/1 instead @callback process_headers(list) :: term @@ -162,31 +172,38 @@ defmodule HTTPoison.Base do # deprecated: Use process_request_url/1 instead @callback process_url(url) :: url - @callback put(url) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} - @callback put(url, body) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + @callback put(url) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} + @callback put(url, body) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback put(url, body, headers) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback put(url, body, headers, options) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} - - @callback put!(url) :: Response.t() | AsyncResponse.t() - @callback put!(url, body) :: Response.t() | AsyncResponse.t() - @callback put!(url, body, headers) :: Response.t() | AsyncResponse.t() - @callback put!(url, body, headers, options) :: Response.t() | AsyncResponse.t() - - @callback request(Request.t()) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} - @callback request(method, url) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} + + @callback put!(url) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback put!(url, body) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback put!(url, body, headers) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback put!(url, body, headers, options) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() + + @callback request(Request.t()) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} + @callback request(method, url) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback request(method, url, body) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback request(method, url, body, headers) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} @callback request(method, url, body, headers, options) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} - @callback request!(method, url) :: Response.t() | AsyncResponse.t() - @callback request!(method, url, body) :: Response.t() | AsyncResponse.t() - @callback request!(method, url, body, headers) :: Response.t() | AsyncResponse.t() - @callback request!(method, url, body, headers, options) :: Response.t() | AsyncResponse.t() + @callback request!(method, url) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback request!(method, url, body) :: Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback request!(method, url, body, headers) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() + @callback request!(method, url, body, headers, options) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() @callback start() :: {:ok, [atom]} | {:error, term} @@ -281,8 +298,21 @@ defmodule HTTPoison.Base do @doc ~S""" Issues an HTTP request using a `Request` struct. - This function returns `{:ok, response}` or `{:ok, async_response}` if the - request is successful, `{:error, reason}` otherwise. + This function returns `{:ok, response}`, `{:ok, async_response}`, or `{:ok, maybe_redirect}` + if the request is successful, `{:error, reason}` otherwise. + + ## Redirect handling + + If the option `:follow_redirect` is given, HTTP redirects are automatically follow if + the method is set to `:get` or `:head` and the response's `status_code` is `301`, `302` or + `307`. + + If the method is set to `:post`, then the only `status_code` that get's automatically + followed is `303`. + + If any other method or `status_code` is returned, then this function returns a + returns a `{:ok, %HTTPoison.MaybeRedirect{}}` containing the `redirect_url` for you to + re-request with the method set to `:get`. ## Examples @@ -296,7 +326,8 @@ defmodule HTTPoison.Base do request(request) """ - @spec request(Request.t()) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + @spec request(Request.t()) :: + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def request(%Request{} = request) do options = process_request_options(request.options) @@ -347,8 +378,21 @@ defmodule HTTPoison.Base do Options: see type `HTTPoison.Request` - This function returns `{:ok, response}` or `{:ok, async_response}` if the - request is successful, `{:error, reason}` otherwise. + This function returns `{:ok, response}`, `{:ok, async_response}`, or `{:ok, maybe_redirect}` + if the request is successful, `{:error, reason}` otherwise. + + ## Redirect handling + + If the option `:follow_redirect` is given, HTTP redirects are automatically follow if + the method is set to `:get` or `:head` and the response's `status_code` is `301`, `302` or + `307`. + + If the method is set to `:post`, then the only `status_code` that get's automatically + followed is `303`. + + If any other method or `status_code` is returned, then this function returns a + returns a `{:ok, %HTTPoison.MaybeRedirect{}}` containing the `redirect_url` for you to + re-request with the method set to `:get`. ## Examples @@ -356,7 +400,7 @@ defmodule HTTPoison.Base do """ @spec request(method, binary, any, headers, Keyword.t()) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def request(method, url, body \\ "", headers \\ [], options \\ []) do request(%Request{ method: method, @@ -376,7 +420,7 @@ defmodule HTTPoison.Base do request fails. """ @spec request!(method, binary, any, headers, Keyword.t()) :: - Response.t() | AsyncResponse.t() + Response.t() | AsyncResponse.t() | MaybeRedirect.t() def request!(method, url, body \\ "", headers \\ [], options \\ []) do case request(method, url, body, headers, options) do {:ok, response} -> response @@ -416,7 +460,7 @@ defmodule HTTPoison.Base do See `request/5` for more detailed information. """ @spec put(binary, any, headers, Keyword.t()) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def put(url, body \\ "", headers \\ [], options \\ []), do: request(:put, url, body, headers, options) @@ -428,7 +472,8 @@ defmodule HTTPoison.Base do See `request!/5` for more detailed information. """ - @spec put!(binary, any, headers, Keyword.t()) :: Response.t() | AsyncResponse.t() + @spec put!(binary, any, headers, Keyword.t()) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() def put!(url, body \\ "", headers \\ [], options \\ []), do: request!(:put, url, body, headers, options) @@ -464,7 +509,7 @@ defmodule HTTPoison.Base do See `request/5` for more detailed information. """ @spec post(binary, any, headers, Keyword.t()) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def post(url, body, headers \\ [], options \\ []), do: request(:post, url, body, headers, options) @@ -476,7 +521,8 @@ defmodule HTTPoison.Base do See `request!/5` for more detailed information. """ - @spec post!(binary, any, headers, Keyword.t()) :: Response.t() | AsyncResponse.t() + @spec post!(binary, any, headers, Keyword.t()) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() def post!(url, body, headers \\ [], options \\ []), do: request!(:post, url, body, headers, options) @@ -489,7 +535,7 @@ defmodule HTTPoison.Base do See `request/5` for more detailed information. """ @spec patch(binary, any, headers, Keyword.t()) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def patch(url, body, headers \\ [], options \\ []), do: request(:patch, url, body, headers, options) @@ -501,7 +547,8 @@ defmodule HTTPoison.Base do See `request!/5` for more detailed information. """ - @spec patch!(binary, any, headers, Keyword.t()) :: Response.t() | AsyncResponse.t() + @spec patch!(binary, any, headers, Keyword.t()) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() def patch!(url, body, headers \\ [], options \\ []), do: request!(:patch, url, body, headers, options) @@ -514,7 +561,7 @@ defmodule HTTPoison.Base do See `request/5` for more detailed information. """ @spec delete(binary, headers, Keyword.t()) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def delete(url, headers \\ [], options \\ []), do: request(:delete, url, "", headers, options) @@ -526,7 +573,8 @@ defmodule HTTPoison.Base do See `request!/5` for more detailed information. """ - @spec delete!(binary, headers, Keyword.t()) :: Response.t() | AsyncResponse.t() + @spec delete!(binary, headers, Keyword.t()) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() def delete!(url, headers \\ [], options \\ []), do: request!(:delete, url, "", headers, options) @@ -539,7 +587,7 @@ defmodule HTTPoison.Base do See `request/5` for more detailed information. """ @spec options(binary, headers, Keyword.t()) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def options(url, headers \\ [], options \\ []), do: request(:options, url, "", headers, options) @@ -551,7 +599,8 @@ defmodule HTTPoison.Base do See `request!/5` for more detailed information. """ - @spec options!(binary, headers, Keyword.t()) :: Response.t() | AsyncResponse.t() + @spec options!(binary, headers, Keyword.t()) :: + Response.t() | AsyncResponse.t() | MaybeRedirect.t() def options!(url, headers \\ [], options \\ []), do: request!(:options, url, "", headers, options) @@ -782,7 +831,7 @@ defmodule HTTPoison.Base do @doc false @spec request(module, request, fun, fun, fun, fun) :: - {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} + {:ok, Response.t() | AsyncResponse.t() | MaybeRedirect.t()} | {:error, Error.t()} def request( module, request, @@ -827,6 +876,15 @@ defmodule HTTPoison.Base do {:error, %Error{reason: reason}} end + {:ok, {:maybe_redirect, status_code, headers, _client}} -> + maybe_redirect( + process_response_status_code, + process_response_headers, + status_code, + headers, + request + ) + {:ok, id} -> {:ok, %HTTPoison.AsyncResponse{id: id}} @@ -880,4 +938,21 @@ defmodule HTTPoison.Base do } |> process_response.()} end + + defp maybe_redirect( + process_response_status_code, + process_response_headers, + status_code, + headers, + request + ) do + {:ok, + %MaybeRedirect{ + status_code: process_response_status_code.(status_code), + headers: process_response_headers.(headers), + request: request, + request_url: request.url, + redirect_url: :proplists.get_value("Location", headers, nil) + }} + end end diff --git a/test/httpoison_base_test.exs b/test/httpoison_base_test.exs index c3f7ac2..16e5309 100644 --- a/test/httpoison_base_test.exs +++ b/test/httpoison_base_test.exs @@ -569,6 +569,33 @@ defmodule HTTPoisonBaseTest do } end + test "request returns MaybeRedirect when passing follow_redirect option" do + expect(:hackney, :request, fn :post, + "http://localhost", + [], + "body", + [follow_redirect: true] -> + # Mock a redirect from `http://localhost` to `https://localhost` + {:ok, {:maybe_redirect, 302, _headers = [{"Location", "https://localhost"}], :client}} + end) + + assert HTTPoison.post!("localhost", "body", [], follow_redirect: true) == + %HTTPoison.MaybeRedirect{ + status_code: 302, + headers: [{"Location", "https://localhost"}], + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [follow_redirect: true], + params: %{}, + url: "http://localhost" + }, + redirect_url: "https://localhost" + } + end + test "passing max_redirect option" do expect(:hackney, :request, fn :post, "http://localhost", [], "body", [max_redirect: 2] -> {:ok, 200, "headers", :client}