Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Steps #17

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 14 additions & 23 deletions lib/http_client/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule HTTPClient.Adapter do
"""

alias HTTPClient.Adapters.{Finch, HTTPoison}
alias HTTPClient.{Error, Response, Telemetry}
alias HTTPClient.{Request, Steps}
alias NimbleOptions.ValidationError

@typedoc """
Expand Down Expand Up @@ -123,50 +123,41 @@ defmodule HTTPClient.Adapter do

@doc false
def request(adapter, method, url, body, headers, options) do
perform(adapter, :request, [method, url, body, headers, options])
perform(adapter, method, url, body: body, headers: headers, options: options)
end

@doc false
def get(adapter, url, headers, options) do
perform(adapter, :get, [url, headers, options])
perform(adapter, :get, url, headers: headers, options: options)
end

@doc false
def post(adapter, url, body, headers, options) do
perform(adapter, :post, [url, body, headers, options])
perform(adapter, :post, url, body: body, headers: headers, options: options)
end

@doc false
def put(adapter, url, body, headers, options) do
perform(adapter, :put, [url, body, headers, options])
perform(adapter, :put, url, body: body, headers: headers, options: options)
end

@doc false
def patch(adapter, url, body, headers, options) do
perform(adapter, :patch, [url, body, headers, options])
perform(adapter, :patch, url, body: body, headers: headers, options: options)
end

@doc false
def delete(adapter, url, headers, options) do
perform(adapter, :delete, [url, headers, options])
perform(adapter, :delete, url, headers: headers, options: options)
end

defp perform(adapter, method, args) do
metadata = %{adapter: adapter, args: args, method: method}
start_time = Telemetry.start(:request, metadata)

case apply(adapter, method, args) do
{:ok, %Response{status: status, headers: headers} = response} ->
metadata = Map.put(metadata, :status_code, status)
Telemetry.stop(:request, start_time, metadata)
headers = Enum.map(headers, fn {key, value} -> {String.downcase(key), value} end)
{:ok, %{response | headers: headers}}

{:error, %Error{reason: reason}} = error_response ->
metadata = Map.put(metadata, :error, reason)
Telemetry.stop(:request, start_time, metadata)
error_response
end
defp perform(adapter, method, url, options) do
steps_options = Keyword.get(options, :options, [])

adapter
|> Request.build(method, url, options)
|> Steps.put_default_steps(steps_options)
|> Request.run()
end

defp adapter_mod(:finch), do: HTTPClient.Adapters.Finch
Expand Down
90 changes: 15 additions & 75 deletions lib/http_client/adapters/finch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,38 @@ defmodule HTTPClient.Adapters.Finch do
Implementation of `HTTPClient.Adapter` behaviour using Finch HTTP client.
"""

alias HTTPClient.{Error, Response}
alias HTTPClient.{Request, Response}

@type method() :: Finch.Request.method()
@type url() :: Finch.Request.url()
@type headers() :: Finch.Request.headers()
@type body() :: Finch.Request.body()
@type options() :: keyword()

@behaviour HTTPClient.Adapter

@delay 1000

@impl true
def request(method, url, body, headers, options) do
perform_request(method, url, headers, body, options)
end

@impl true
def get(url, headers, options) do
perform_request(:get, url, headers, nil, options)
end

@impl true
def post(url, body, headers, options) do
perform_request(:post, url, headers, body, options)
end

@impl true
def put(url, body, headers, options) do
perform_request(:put, url, headers, body, options)
end

@impl true
def patch(url, body, headers, options) do
perform_request(:patch, url, headers, body, options)
end

@impl true
def delete(url, headers, options) do
perform_request(:delete, url, headers, nil, options)
end

defp perform_request(method, url, headers, body, options, attempt \\ 0) do
{params, options} = Keyword.pop(options, :params)
{basic_auth, options} = Keyword.pop(options, :basic_auth)

url = build_request_url(url, params)
headers = add_basic_auth_header(headers, basic_auth)
@doc """
Performs the request using `Finch`.
"""
def perform_request(request, options \\ []) do
options = prepare_options(options)

method
|> Finch.build(url, headers, body)
|> Finch.request(get_client(), options)
request.method
|> Finch.build(request.url, request.headers, request.body)
|> Finch.request(request.private.finch_name, options)
|> case do
{:ok, %{status: status, body: body, headers: headers}} ->
{:ok, %Response{status: status, body: body, headers: headers, request_url: url}}

{:error,
%Mint.HTTPError{
reason: {:proxy, _}
}} ->
case attempt < 5 do
true ->
Process.sleep(attempt * @delay)
perform_request(method, url, headers, body, options, attempt + 1)

false ->
{:error, %Error{reason: :proxy_error}}
end
{request,
Response.new(status: status, body: body, headers: headers, request_url: request.url)}

{:error, error} ->
{:error, %Error{reason: error.reason}}
{:error, exception} ->
{request, exception}
end
end

defp build_request_url(url, nil), do: url

defp 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
@doc false
def proxy(request) do
Request.put_private(request, :finch_name, get_client())
end

defp add_basic_auth_header(headers, {username, password}) do
credentials = Base.encode64("#{username}:#{password}")
[{"Authorization", "Basic " <> credentials} | headers || []]
end

defp add_basic_auth_header(headers, _basic_auth), do: headers

defp prepare_options(options) do
Enum.map(options, &normalize_option/1)
end
Expand Down
74 changes: 15 additions & 59 deletions lib/http_client/adapters/httpoison.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,31 @@ defmodule HTTPClient.Adapters.HTTPoison do
Implementation of `HTTPClient.Adapter` behaviour using HTTPoison HTTP client.
"""

alias HTTPClient.{Error, Response}
alias HTTPClient.Response

@type method() :: HTTPoison.Request.method()
@type url() :: HTTPoison.Request.url()
@type headers() :: HTTPoison.Request.headers()
@type body() :: HTTPoison.Request.body()
@type options() :: HTTPoison.Request.options()

@behaviour HTTPClient.Adapter

@delay 1000

@impl true
def request(method, url, body, headers, options) do
perform_request(method, url, headers, body, options)
end

@impl true
def get(url, headers, options) do
perform_request(:get, url, headers, "", options)
end

@impl true
def post(url, body, headers, options) do
perform_request(:post, url, headers, body, options)
end

@impl true
def put(url, body, headers, options) do
perform_request(:put, url, headers, body, options)
end

@impl true
def patch(url, body, headers, options) do
perform_request(:patch, url, headers, body, options)
end

@impl true
def delete(url, headers, options) do
perform_request(:delete, url, headers, "", options)
end

defp perform_request(method, url, headers, body, options, attempt \\ 0) do
options = setup_proxy(options)
options = add_basic_auth_option(options, options[:basic_auth])

case HTTPoison.request(method, url, body, headers, options) do
{:ok, %{status_code: status, body: body, headers: headers, request: request}} ->
{:ok, %Response{status: status, body: body, headers: headers, request_url: request.url}}

{:error, %HTTPoison.Error{id: nil, reason: :proxy_error}} ->
case attempt < 5 do
true ->
Process.sleep(attempt * @delay)
perform_request(method, url, headers, body, options, attempt + 1)

false ->
{:error, %Error{reason: :proxy_error}}
end

{:error, error} ->
{:error, %Error{reason: error.reason}}
@doc """
Performs the request using `HTTPoison`.
"""
def perform_request(request, options \\ []) do
case HTTPoison.request(request.method, request.url, request.body, request.headers, options) do
{:ok, %{status_code: status, body: body, headers: headers}} ->
{request,
Response.new(status: status, body: body, headers: headers, request_url: request.url)}

{:error, exception} ->
{request, exception}
end
end

defp add_basic_auth_option(options, nil), do: options

defp add_basic_auth_option(options, {username, password}) do
put_in(options, [:hackney], basic_auth: {username, password})
@doc false
def proxy(request) do
update_in(request.adapter_options, &setup_proxy/1)
end

defp setup_proxy(options) do
Expand Down
Loading