Skip to content

Commit 161faf1

Browse files
committed
Introduce Steps
* Add exponent retry * Dynamically start Finch pool with the given proxy configs * Support multiple content-encoding headers * Add follow_redirects step
1 parent 2b0fe05 commit 161faf1

14 files changed

Lines changed: 1120 additions & 264 deletions

File tree

.github/workflows/main.yml

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,37 @@ name: CI
33
on: [push, pull_request]
44

55
jobs:
6-
format:
7-
name: Format and compile with warnings as errors
8-
runs-on: ubuntu-latest
9-
steps:
10-
- uses: actions/checkout@v4
11-
12-
- name: Install OTP and Elixir
13-
uses: erlef/setup-beam@v1
14-
with:
15-
otp-version: 27.x
16-
elixir-version: 1.18.x
17-
18-
- name: Install dependencies
19-
run: mix deps.get
20-
21-
- name: Run "mix format"
22-
run: mix format --check-formatted
23-
24-
- name: Compile with --warnings-as-errors
25-
run: mix compile --warnings-as-errors
26-
276
test:
28-
name: Test
297
runs-on: ubuntu-latest
8+
env:
9+
MIX_ENV: test
3010
strategy:
3111
fail-fast: false
3212
matrix:
3313
include:
34-
- erlang: 26.x
35-
elixir: 1.16.x
36-
- erlang: 27.x
37-
elixir: 1.19.x
14+
- pair:
15+
elixir: 1.16.x
16+
otp: 26.x
17+
- pair:
18+
elixir: 1.19.x
19+
otp: 27.x
3820
steps:
39-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v6
4022

4123
- name: Install OTP and Elixir
42-
uses: erlef/setup-elixir@v1
24+
uses: erlef/setup-beam@v1
4325
with:
44-
otp-version: ${{matrix.erlang}}
26+
otp-version: ${{matrix.otp}}
4527
elixir-version: ${{matrix.elixir}}
4628

4729
- name: Install dependencies
4830
run: mix deps.get
4931

50-
- name: Run tests
51-
run: mix test --trace
32+
- name: Run "mix format"
33+
run: mix format --check-formatted
34+
35+
- name: Check unused dependencies
36+
run: mix deps.unlock --check-unused
37+
38+
- name: Compile with --warnings-as-errors
39+
run: mix compile --warnings-as-errors

lib/http_client/adapter.ex

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defmodule HTTPClient.Adapter do
1616
"""
1717

1818
alias HTTPClient.Adapters.{Finch, HTTPoison}
19-
alias HTTPClient.{Error, Response, Telemetry}
19+
alias HTTPClient.{Request, Steps}
2020
alias NimbleOptions.ValidationError
2121

2222
@typedoc """
@@ -123,50 +123,39 @@ defmodule HTTPClient.Adapter do
123123

124124
@doc false
125125
def request(adapter, method, url, body, headers, options) do
126-
perform(adapter, :request, [method, url, body, headers, options])
126+
perform(adapter, method, url, body: body, headers: headers, options: options)
127127
end
128128

129129
@doc false
130130
def get(adapter, url, headers, options) do
131-
perform(adapter, :get, [url, headers, options])
131+
perform(adapter, :get, url, headers: headers, options: options)
132132
end
133133

134134
@doc false
135135
def post(adapter, url, body, headers, options) do
136-
perform(adapter, :post, [url, body, headers, options])
136+
perform(adapter, :post, url, body: body, headers: headers, options: options)
137137
end
138138

139139
@doc false
140140
def put(adapter, url, body, headers, options) do
141-
perform(adapter, :put, [url, body, headers, options])
141+
perform(adapter, :put, url, body: body, headers: headers, options: options)
142142
end
143143

144144
@doc false
145145
def patch(adapter, url, body, headers, options) do
146-
perform(adapter, :patch, [url, body, headers, options])
146+
perform(adapter, :patch, url, body: body, headers: headers, options: options)
147147
end
148148

149149
@doc false
150150
def delete(adapter, url, headers, options) do
151-
perform(adapter, :delete, [url, headers, options])
151+
perform(adapter, :delete, url, headers: headers, options: options)
152152
end
153153

154-
defp perform(adapter, method, args) do
155-
metadata = %{adapter: adapter, args: args, method: method}
156-
start_time = Telemetry.start(:request, metadata)
157-
158-
case apply(adapter, method, args) do
159-
{:ok, %Response{status: status, headers: headers} = response} ->
160-
metadata = Map.put(metadata, :status_code, status)
161-
Telemetry.stop(:request, start_time, metadata)
162-
headers = Enum.map(headers, fn {key, value} -> {String.downcase(key), value} end)
163-
{:ok, %{response | headers: headers}}
164-
165-
{:error, %Error{reason: reason}} = error_response ->
166-
metadata = Map.put(metadata, :error, reason)
167-
Telemetry.stop(:request, start_time, metadata)
168-
error_response
169-
end
154+
defp perform(adapter, method, url, options) do
155+
adapter
156+
|> Request.build(method, url, options)
157+
|> Steps.put_default_steps()
158+
|> Request.run()
170159
end
171160

172161
defp adapter_mod(:finch), do: HTTPClient.Adapters.Finch

lib/http_client/adapters/finch.ex

Lines changed: 59 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,118 +3,97 @@ defmodule HTTPClient.Adapters.Finch do
33
Implementation of `HTTPClient.Adapter` behaviour using Finch HTTP client.
44
"""
55

6-
alias HTTPClient.{Error, Response}
6+
alias HTTPClient.{Request, Response}
77

88
@type method() :: Finch.Request.method()
99
@type url() :: Finch.Request.url()
1010
@type headers() :: Finch.Request.headers()
1111
@type body() :: Finch.Request.body()
1212
@type options() :: keyword()
1313

14-
@behaviour HTTPClient.Adapter
14+
@doc """
15+
Performs the request using `Finch`.
16+
"""
17+
def perform_request(request) do
18+
options = prepare_options(request.options)
1519

16-
@delay 1000
20+
request.method
21+
|> Finch.build(request.url, request.headers, request.body)
22+
|> Finch.request(request.private.finch_name, options)
23+
|> case do
24+
{:ok, %{status: status, body: body, headers: headers}} ->
25+
{request,
26+
Response.new(status: status, body: body, headers: headers, request_url: request.url)}
1727

18-
@impl true
19-
def request(method, url, body, headers, options) do
20-
perform_request(method, url, headers, body, options)
28+
{:error, exception} ->
29+
{request, exception}
30+
end
2131
end
2232

23-
@impl true
24-
def get(url, headers, options) do
25-
perform_request(:get, url, headers, nil, options)
33+
@doc false
34+
def proxy(request) do
35+
Request.put_private(request, :finch_name, get_client())
2636
end
2737

28-
@impl true
29-
def post(url, body, headers, options) do
30-
perform_request(:post, url, headers, body, options)
38+
defp prepare_options(options) do
39+
Enum.map(options, &normalize_option/1)
3140
end
3241

33-
@impl true
34-
def put(url, body, headers, options) do
35-
perform_request(:put, url, headers, body, options)
36-
end
42+
defp normalize_option({:timeout, value}), do: {:pool_timeout, value}
43+
defp normalize_option({:recv_timeout, value}), do: {:receive_timeout, value}
44+
defp normalize_option({key, value}), do: {key, value}
3745

38-
@impl true
39-
def patch(url, body, headers, options) do
40-
perform_request(:patch, url, headers, body, options)
46+
defp get_client() do
47+
:http_client
48+
|> Application.get_env(:proxy)
49+
|> get_client_name()
4150
end
4251

43-
@impl true
44-
def delete(url, headers, options) do
45-
perform_request(:delete, url, headers, nil, options)
52+
defp get_client_name(nil), do: HTTPClient.Finch
53+
54+
defp get_client_name(proxies) when is_list(proxies) do
55+
proxies
56+
|> Enum.random()
57+
|> get_client_name()
4658
end
4759

48-
defp perform_request(method, url, headers, body, options, attempt \\ 0) do
49-
{params, options} = Keyword.pop(options, :params)
50-
{basic_auth, options} = Keyword.pop(options, :basic_auth)
60+
defp get_client_name(proxy) when is_map(proxy) do
61+
name = custom_pool_name(proxy)
5162

52-
url = build_request_url(url, params)
53-
headers = add_basic_auth_header(headers, basic_auth)
54-
options = prepare_options(options)
63+
pools = %{
64+
default: [
65+
conn_opts: [proxy: compose_proxy(proxy), proxy_headers: compose_proxy_headers(proxy)]
66+
]
67+
}
5568

56-
method
57-
|> Finch.build(url, headers, body)
58-
|> Finch.request(get_client(), options)
59-
|> case do
60-
{:ok, %{status: status, body: body, headers: headers}} ->
61-
{:ok, %Response{status: status, body: body, headers: headers, request_url: url}}
62-
63-
{:error,
64-
%Mint.HTTPError{
65-
reason: {:proxy, _}
66-
}} ->
67-
case attempt < 5 do
68-
true ->
69-
Process.sleep(attempt * @delay)
70-
perform_request(method, url, headers, body, options, attempt + 1)
71-
72-
false ->
73-
{:error, %Error{reason: :proxy_error}}
74-
end
75-
76-
{:error, error} ->
77-
{:error, %Error{reason: error.reason}}
78-
end
79-
end
80-
81-
defp build_request_url(url, nil), do: url
69+
child_spec = {Finch, name: name, pools: pools}
8270

83-
defp build_request_url(url, params) do
84-
cond do
85-
Enum.count(params) === 0 -> url
86-
URI.parse(url).query -> url <> "&" <> URI.encode_query(params)
87-
true -> url <> "?" <> URI.encode_query(params)
71+
case DynamicSupervisor.start_child(HTTPClient.FinchSupervisor, child_spec) do
72+
{:ok, _} -> name
73+
{:error, {:already_started, _}} -> name
8874
end
8975
end
9076

91-
defp add_basic_auth_header(headers, {username, password}) do
92-
credentials = Base.encode64("#{username}:#{password}")
93-
[{"Authorization", "Basic " <> credentials} | headers || []]
77+
defp compose_proxy_headers(%{opts: opts}) do
78+
Keyword.get(opts, :proxy_headers, [])
9479
end
9580

96-
defp add_basic_auth_header(headers, _basic_auth), do: headers
81+
defp compose_proxy_headers(_), do: []
9782

98-
defp prepare_options(options) do
99-
Enum.map(options, &normalize_option/1)
83+
defp compose_proxy(proxy) do
84+
{proxy.scheme, proxy.address, to_integer(proxy.port), proxy.opts}
10085
end
10186

102-
defp normalize_option({:timeout, value}), do: {:pool_timeout, value}
103-
defp normalize_option({:recv_timeout, value}), do: {:receive_timeout, value}
104-
defp normalize_option({key, value}), do: {key, value}
105-
106-
defp get_client do
107-
case Application.get_env(:http_client, :proxy, nil) do
108-
nil -> FinchHTTPClient
109-
proxies -> get_client_with_proxy(proxies)
110-
end
111-
end
87+
defp to_integer(term) when is_integer(term), do: term
88+
defp to_integer(term) when is_binary(term), do: String.to_integer(term)
11289

113-
defp get_client_with_proxy(proxy) when is_map(proxy) do
114-
FinchHTTPClientWithProxy_0
115-
end
90+
defp custom_pool_name(opts) do
91+
name =
92+
opts
93+
|> :erlang.term_to_binary()
94+
|> :erlang.md5()
95+
|> Base.url_encode64(padding: false)
11696

117-
defp get_client_with_proxy(proxies) when is_list(proxies) do
118-
:"FinchHTTPClientWithProxy_#{Enum.random(0..(length(proxies) - 1))}"
97+
Module.concat(HTTPClient.FinchSupervisor, "Pool_#{name}")
11998
end
12099
end

lib/http_client/adapters/finch/config.ex

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)