diff --git a/lib/httpoison.ex b/lib/httpoison.ex index 22a93ab..bd89623 100644 --- a/lib/httpoison.ex +++ b/lib/httpoison.ex @@ -1,6 +1,33 @@ +defmodule HTTPoison.Request do + @enforce_keys [:url] + defstruct method: :get, url: nil, headers: [], body: "", params: %{}, options: [] + @type method :: :get | :post | :put | :patch | :delete | :options | :head + @type headers :: [{atom, binary}] | [{binary, binary}] | %{binary => binary} + @type url :: binary + @type body :: binary | {:form, [{atom, any}]} | {:file, binary} + @type params :: map | keyword | [{binary, binary}] + @type options :: keyword + @type t :: %__MODULE__{ + method: method, + url: binary, + headers: headers, + body: body, + params: params, + options: options + } +end + defmodule HTTPoison.Response do - defstruct status_code: nil, body: nil, headers: [], request_url: nil - @type t :: %__MODULE__{status_code: integer, body: term, headers: list} + defstruct status_code: nil, body: nil, headers: [], request_url: nil, request: nil + alias HTTPoison.Request + + @type t :: %__MODULE__{ + status_code: integer, + body: term, + headers: list, + request: Request.t(), + request_url: Request.url() + } end defmodule HTTPoison.AsyncResponse do diff --git a/lib/httpoison/base.ex b/lib/httpoison/base.ex index a61faca..79cdf48 100644 --- a/lib/httpoison/base.ex +++ b/lib/httpoison/base.ex @@ -67,11 +67,12 @@ defmodule HTTPoison.Base do # Used to arbitrarily process the status code of a response before # returning it to the caller. - @spec process_status_code(integer) :: term - def process_status_code(status_code) + @spec process_response_status_code(integer) :: term + def process_response_status_code(status_code) """ + alias HTTPoison.Request alias HTTPoison.Response alias HTTPoison.AsyncResponse alias HTTPoison.Error @@ -132,21 +133,24 @@ defmodule HTTPoison.Base do @callback post!(url, term, headers) :: Response.t() | AsyncResponse.t() @callback post!(url, term, headers, options) :: Response.t() | AsyncResponse.t() - @callback process_headers(headers) :: headers + @callback process_headers(response | headers) :: headers + @callback process_response_headers(response | headers) :: headers - @callback process_request_body(term) :: body + @callback process_request_body(request | term) :: body - @callback process_request_headers(headers) :: headers + @callback process_request_headers(request | headers) :: headers - @callback process_request_options(options) :: options + @callback process_request_options(request | options) :: options - @callback process_response_body(binary) :: term + @callback process_response_body(response | binary) :: term @callback process_response_chunk(binary) :: term - @callback process_status_code(integer) :: term + @callback process_status_code(response | integer) :: term + @callback process_response_status_code(response | integer) :: term - @callback process_url(binary) :: binary + @callback process_url(request | term) :: url + @callback process_request_url(request | term) :: url @callback put(binary) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} @callback put(binary, term) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} @@ -175,55 +179,101 @@ defmodule HTTPoison.Base do @callback stream_next(AsyncResponse.t()) :: {:ok, AsyncResponse.t()} | {:error, Error.t()} - @type url :: binary - @type headers :: [{atom, binary}] | [{binary, binary}] | %{binary => binary} - @type body :: binary | {:form, [{atom, any}]} | {:file, binary} - @type options :: Keyword.t() + @type response :: Response.t() + @type request :: Request.t() + @type url :: Request.url() + @type headers :: Request.headers() + @type body :: Request.body() + @type options :: Request.options() + @type params :: Request.params() defmacro __using__(_) do quote do @behaviour HTTPoison.Base + + @type response :: HTTPoison.Base.response() + @type request :: HTTPoison.Base.request() + @type url :: HTTPoison.Base.url() @type headers :: HTTPoison.Base.headers() @type body :: HTTPoison.Base.body() + @type options :: HTTPoison.Base.options() + @type params :: HTTPoison.Base.params() @doc """ Starts HTTPoison and its dependencies. """ def start, do: :application.ensure_all_started(:httpoison) + @deprecated "Use process_request_url/1 instead" + @spec process_url(request | url) :: url def process_url(url) do - HTTPoison.Base.default_process_url(url) + HTTPoison.Base.default_process_request_url(url) end - @spec process_request_body(any) :: body + @spec process_request_url(request | url) :: url + def process_request_url(%Request{url: url}), do: process_request_url(url) + + def process_request_url(url), do: process_url(url) + + @spec process_request_body(request | any) :: body + def process_request_body(%Request{body: body}), do: process_request_body(body) + def process_request_body(body), do: body - @spec process_response_body(binary) :: any - def process_response_body(body), do: body + @spec process_request_headers(request | headers) :: headers + def process_request_headers(%Request{headers: headers}) do + process_request_headers(headers) + end - @spec process_request_headers(headers) :: headers def process_request_headers(headers) when is_map(headers) do Enum.into(headers, []) end def process_request_headers(headers), do: headers + @spec process_request_options(request | options) :: options + def process_request_options(%Request{options: options}) do + process_request_options(options) + end + def process_request_options(options), do: options - def process_response_chunk(chunk), do: chunk + @spec process_request_params(request | any) :: params + def process_request_params(%Request{params: params}), do: process_request_params(params) + def process_request_params(params), do: params + + @spec process_response(response) :: any + def process_response(%Response{} = response), do: response + + @deprecated "Use process_response_headers/1 instead" + @spec process_headers(list) :: any def process_headers(headers), do: headers + @spec process_response_headers(response | list) :: any + def process_response_headers(headers), do: process_headers(headers) + + @deprecated "Use process_response_status_code/1 instead" + @spec process_status_code(integer) :: any def process_status_code(status_code), do: status_code + @spec process_response_status_code(integer) :: any + def process_response_status_code(status_code), do: process_status_code(status_code) + + @spec process_response_body(binary) :: any + def process_response_body(body), do: body + + @spec process_response_chunk(binary) :: any + def process_response_chunk(chunk), do: chunk + @doc false @spec transformer(pid) :: :ok def transformer(target) do HTTPoison.Base.transformer( __MODULE__, target, - &process_status_code/1, - &process_headers/1, + &process_response_status_code/1, + &process_response_headers/1, &process_response_chunk/1 ) end @@ -273,33 +323,66 @@ defmodule HTTPoison.Base do request(:post, "https://my.website.com", "{\"foo\": 3}", [{"Accept", "application/json"}]) """ + + def request(%Request{} = request) do + params = + request.params + |> HTTPoison.Base.merge_params(request.options[:params]) + |> process_request_params() + + url = + request + |> process_request_url() + |> HTTPoison.Base.build_request_url(params) + + request = %Request{ + method: request.method, + url: url, + headers: process_request_headers(request), + body: process_request_body(request), + params: params, + options: process_request_options(request) + } + + HTTPoison.Base.request( + __MODULE__, + request, + &process_response_status_code/1, + &process_response_headers/1, + &process_response_body/1, + &process_response/1 + ) + end + @spec request(atom, binary, any, headers, Keyword.t()) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} def request(method, url, body \\ "", headers \\ [], options \\ []) do options = process_request_options(options) + params = process_request_params(options[:params] || %{}) url = - cond do - not Keyword.has_key?(options, :params) -> url - URI.parse(url).query -> url <> "&" <> URI.encode_query(options[:params]) - true -> url <> "?" <> URI.encode_query(options[:params]) - end - - url = process_url(to_string(url)) - body = process_request_body(body) - headers = process_request_headers(headers) + url + |> to_string() + |> process_request_url() + |> HTTPoison.Base.build_request_url(params) + + request = %Request{ + method: method, + url: url, + headers: process_request_headers(headers), + body: process_request_body(body), + params: params, + options: options + } HTTPoison.Base.request( __MODULE__, - method, - url, - body, - headers, - options, - &process_status_code/1, - &process_headers/1, - &process_response_body/1 + request, + &process_response_status_code/1, + &process_response_headers/1, + &process_response_body/1, + &process_response/1 ) end @@ -508,15 +591,35 @@ defmodule HTTPoison.Base do end @doc false - def transformer(module, target, process_status_code, process_headers, process_response_chunk) do + def transformer( + module, + target, + process_response_status_code, + process_response_headers, + process_response_chunk + ) do receive do {:hackney_response, id, {:status, code, _reason}} -> - send(target, %HTTPoison.AsyncStatus{id: id, code: process_status_code.(code)}) - transformer(module, target, process_status_code, process_headers, process_response_chunk) + send(target, %HTTPoison.AsyncStatus{id: id, code: process_response_status_code.(code)}) + + transformer( + module, + target, + process_response_status_code, + process_response_headers, + process_response_chunk + ) {:hackney_response, id, {:headers, headers}} -> - send(target, %HTTPoison.AsyncHeaders{id: id, headers: process_headers.(headers)}) - transformer(module, target, process_status_code, process_headers, process_response_chunk) + send(target, %HTTPoison.AsyncHeaders{id: id, headers: process_response_headers.(headers)}) + + transformer( + module, + target, + process_response_status_code, + process_response_headers, + process_response_chunk + ) {:hackney_response, id, :done} -> send(target, %HTTPoison.AsyncEnd{id: id}) @@ -525,16 +628,27 @@ defmodule HTTPoison.Base do send(target, %Error{id: id, reason: reason}) {:hackney_response, id, {redirect, to, headers}} when redirect in [:redirect, :see_other] -> - send(target, %HTTPoison.AsyncRedirect{id: id, to: to, headers: process_headers.(headers)}) + send(target, %HTTPoison.AsyncRedirect{ + id: id, + to: to, + headers: process_response_headers.(headers) + }) {:hackney_response, id, chunk} -> send(target, %HTTPoison.AsyncChunk{id: id, chunk: process_response_chunk.(chunk)}) - transformer(module, target, process_status_code, process_headers, process_response_chunk) + + transformer( + module, + target, + process_response_status_code, + process_response_headers, + process_response_chunk + ) end end @doc false - def default_process_url(url) do + def default_process_request_url(url) do case url |> String.slice(0, 12) |> String.downcase() do "http://" <> _ -> url "https://" <> _ -> url @@ -543,7 +657,28 @@ defmodule HTTPoison.Base do end end - defp build_hackney_options(module, options) do + @doc false + def merge_params(params, nil), do: params + + def merge_params(request_params, options_params) do + Map.merge( + URI.encode_query(request_params) |> URI.decode_query(), + URI.encode_query(options_params) |> URI.decode_query() + ) + end + + @doc false + def build_request_url(url, nil), do: url + + def build_request_url(url, params) do + cond do + Enum.count(params) === 0 -> url + URI.parse(url).query -> url <> "&" <> URI.encode_query(params) + true -> url <> "?" <> URI.encode_query(params) + end + end + + defp build_hackney_options(module, %Request{options: options}) do timeout = Keyword.get(options, :timeout) recv_timeout = Keyword.get(options, :recv_timeout) stream_to = Keyword.get(options, :stream_to) @@ -583,7 +718,7 @@ defmodule HTTPoison.Base do hn_options end - defp build_hackney_proxy_options(options, request_url) do + defp build_hackney_proxy_options(%Request{options: options, url: request_url}) do proxy = if Keyword.has_key?(options, :proxy) do Keyword.get(options, :proxy) @@ -614,47 +749,46 @@ defmodule HTTPoison.Base do end @doc false - @spec request(atom, atom, binary, body, headers, any, fun, fun, fun) :: + @spec request(atom, request, fun, fun, fun, fun) :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()} def request( module, - method, - request_url, - request_body, - request_headers, - options, - process_status_code, - process_headers, - process_response_body + request, + process_response_status_code, + process_response_headers, + process_response_body, + process_response ) do - hn_proxy_options = build_hackney_proxy_options(options, request_url) - hn_options = hn_proxy_options ++ build_hackney_options(module, options) + hn_proxy_options = build_hackney_proxy_options(request) + hn_options = hn_proxy_options ++ build_hackney_options(module, request) - case do_request(method, request_url, request_headers, request_body, hn_options) do + case do_request(request, hn_options) do {:ok, status_code, headers} -> response( - process_status_code, - process_headers, + process_response_status_code, + process_response_headers, process_response_body, + process_response, status_code, headers, "", - request_url + request ) {:ok, status_code, headers, client} -> - max_length = Keyword.get(options, :max_body_length, :infinity) + max_length = Keyword.get(request.options, :max_body_length, :infinity) case :hackney.body(client, max_length) do {:ok, body} -> response( - process_status_code, - process_headers, + process_response_status_code, + process_response_headers, process_response_body, + process_response, status_code, headers, body, - request_url + request ) {:error, reason} -> @@ -669,8 +803,9 @@ defmodule HTTPoison.Base do end end - defp do_request(method, request_url, request_headers, {:stream, enumerable}, hn_options) do - with {:ok, ref} <- :hackney.request(method, request_url, request_headers, :stream, hn_options) do + defp do_request(%Request{body: {:stream, enumerable}} = request, hn_options) do + with {:ok, ref} <- + :hackney.request(request.method, request.url, request.headers, :stream, hn_options) do failures = Stream.transform(enumerable, :ok, fn _, :error -> {:halt, :error} @@ -689,25 +824,28 @@ defmodule HTTPoison.Base do end end - defp do_request(method, request_url, request_headers, request_body, hn_options) do - :hackney.request(method, request_url, request_headers, request_body, hn_options) + defp do_request(request, hn_options) do + :hackney.request(request.method, request.url, request.headers, request.body, hn_options) end defp response( - process_status_code, - process_headers, + process_response_status_code, + process_response_headers, process_response_body, + process_response, status_code, headers, body, - request_url + request ) do {:ok, %Response{ - status_code: process_status_code.(status_code), - headers: process_headers.(headers), + status_code: process_response_status_code.(status_code), + headers: process_response_headers.(headers), body: process_response_body.(body), - request_url: request_url - }} + request: request, + request_url: request.url + } + |> process_response.()} end end diff --git a/test/httpoison_base_test.exs b/test/httpoison_base_test.exs index d791ea3..d237968 100644 --- a/test/httpoison_base_test.exs +++ b/test/httpoison_base_test.exs @@ -4,7 +4,7 @@ defmodule HTTPoisonBaseTest do setup :verify_on_exit! - defmodule Example do + defmodule DeprecatedExample do use HTTPoison.Base def process_url(url), do: "http://" <> url def process_request_body(body), do: {:req_body, body} @@ -15,6 +15,18 @@ defmodule HTTPoisonBaseTest do def process_status_code(code), do: {:code, code} end + defmodule Example do + use HTTPoison.Base + def process_request_url(url), do: "http://" <> url + def process_request_body(body), do: {:req_body, body} + def process_request_headers(headers), do: {:req_headers, headers} + def process_request_options(options), do: Keyword.put(options, :timeout, 10) + def process_response_body(body), do: {:resp_body, body} + def process_response_headers(headers), do: {:headers, headers} + def process_response_status_code(code), do: {:code, code} + def process_response(response), do: {:resp, response} + end + defmodule ExampleParamsOptions do use HTTPoison.Base def process_url(url), do: "http://" <> url @@ -44,11 +56,45 @@ defmodule HTTPoisonBaseTest do expect(:hackney, :body, fn _, _ -> {:ok, "response"} end) assert Example.post!("localhost", "body") == + {:resp, + %HTTPoison.Response{ + status_code: {:code, 200}, + headers: {:headers, "headers"}, + body: {:resp_body, "response"}, + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: {:req_body, "body"}, + headers: {:req_headers, []}, + method: :post, + options: [timeout: 10], + params: %{}, + url: "http://localhost" + } + }} + end + + test "request body using DeprecatedExample" do + expect(:hackney, :request, fn + :post, "http://localhost", {:req_headers, []}, {:req_body, "body"}, [connect_timeout: 10] -> + {:ok, 200, "headers", :client} + end) + + expect(:hackney, :body, fn _, _ -> {:ok, "response"} end) + + assert DeprecatedExample.post!("localhost", "body") == %HTTPoison.Response{ status_code: {:code, 200}, headers: {:headers, "headers"}, body: {:resp_body, "response"}, - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: {:req_body, "body"}, + headers: {:req_headers, []}, + method: :post, + options: [timeout: 10], + params: %{}, + url: "http://localhost" + } } end @@ -64,7 +110,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost?foo=bar&key=fizz" + request_url: "http://localhost?foo=bar&key=fizz", + request: %HTTPoison.Request{ + body: "", + headers: [], + method: :get, + options: [params: %{foo: "bar", key: "fizz"}], + params: %{foo: "bar", key: "fizz"}, + url: "http://localhost?foo=bar&key=fizz" + } } end @@ -94,7 +148,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [timeout: 12345], + params: %{}, + url: "http://localhost" + } } end @@ -111,7 +173,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [recv_timeout: 12345], + params: %{}, + url: "http://localhost" + } } end @@ -127,7 +197,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [proxy: "proxy"], + params: %{}, + url: "http://localhost" + } } end @@ -159,7 +237,19 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [ + proxy: {:socks5, 'localhost', 1080}, + socks5_user: "user", + socks5_pass: "secret" + ], + params: %{}, + url: "http://localhost" + } } end @@ -186,7 +276,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [proxy: "proxy", proxy_auth: {"username", "password"}], + params: %{}, + url: "http://localhost" + } } end @@ -204,7 +302,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [], + params: %{}, + url: "http://localhost" + } } end @@ -222,7 +328,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [], + params: %{}, + url: "http://localhost" + } } end @@ -240,7 +354,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "https://localhost" + request_url: "https://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [], + params: %{}, + url: "https://localhost" + } } end @@ -258,7 +380,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [], + params: %{}, + url: "http://localhost" + } } end @@ -278,7 +408,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [ssl: [certfile: "certs/client.crt"]], + params: %{}, + url: "http://localhost" + } } end @@ -298,7 +436,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [follow_redirect: true], + params: %{}, + url: "http://localhost" + } } end @@ -314,7 +460,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "body", + headers: [], + method: :post, + options: [max_redirect: 2], + params: %{}, + url: "http://localhost" + } } end @@ -331,7 +485,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "response", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "", + headers: [], + method: :get, + options: [], + params: %{}, + url: "http://localhost" + } }} expect(:hackney, :request, fn :get, "http://localhost", [], "", [] -> @@ -346,7 +508,15 @@ defmodule HTTPoisonBaseTest do status_code: 200, headers: "headers", body: "res", - request_url: "http://localhost" + request_url: "http://localhost", + request: %HTTPoison.Request{ + body: "", + headers: [], + method: :get, + options: [max_body_length: 3], + params: %{}, + url: "http://localhost" + } }} end end