Skip to content

Commit

Permalink
Merge pull request #124 from pow-auth/add-finch
Browse files Browse the repository at this point in the history
Add Finch adapter
  • Loading branch information
danschultzer committed Aug 26, 2023
2 parents 37dfd78 + a1022a4 commit c65759d
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v0.2.6 (TBA)

* Added `Assent.HTTPAdapter.Finch`
* Deprecated `Assent.HTTPAdapter.Mint`

## v0.2.5 (2023-08-21)

* `Assent.Strategy.Spotify` added
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def application do
end
```

This is not necessary if you'll use another HTTP adapter, such as Mint.
This is not necessary if you'll use another HTTP adapter like Finch.

Assent requires Erlang OTP 22.1 or greater.

Expand Down Expand Up @@ -197,27 +197,28 @@ end

By default Erlangs built-in `:httpc` is used for requests. SSL verification is automatically enabled when `:certifi` and `:ssl_verify_fun` packages are available. `:httpc` only supports HTTP/1.1.

If you would like HTTP/2 support, you should consider adding [`Mint`](https://github.com/ericmj/mint) to your project.
If you would like HTTP/2 support, you should consider adding [`Finch`](https://github.com/sneako/finch) to your project.

### Finch

Update `mix.exs`:

```elixir
defp deps do
[
# ...
{:mint, "~> 1.0"},
{:castore, "~> 1.0"} # Required for SSL validation
{:finch, "~> 0.16"}
]
end
```

Pass the `:http_adapter` with your provider configuration:
Ensure you start the Finch supervisor in application, and pass `:http_adapter` with your provider configuration using your connection pool:

```elixir
config = [
client_id: "REPLACE_WITH_CLIENT_ID",
client_secret: "REPLACE_WITH_CLIENT_SECRET",
http_adapter: Assent.HTTPAdapter.Mint
http_adapter: {Assent.HTTPAdapter.Finch, supervisor: MyFinch}
]
```

Expand Down
35 changes: 35 additions & 0 deletions lib/assent/http_adapter/finch.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
if Code.ensure_loaded?(Finch) do
defmodule Assent.HTTPAdapter.Finch do
@moduledoc """
HTTP adapter module for making http requests with Finch.
The Finch adapter must be configured with the supervisor by passing it as an
option:
http_adapter: {Assent.HTTPAdapter.Finch, [supervisor: MyFinch]}
See `Assent.HTTPAdapter` for more.
"""
alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse}

@behaviour HTTPAdapter

@impl HTTPAdapter
def request(method, url, body, headers, finch_opts \\ nil) do
headers = headers ++ [HTTPAdapter.user_agent_header()]
opts = finch_opts || []

supervisor = Keyword.get(opts, :supervisor) || raise "Missing `:supervisor` option for the #{inspect __MODULE__} configuration"
build_opts = Keyword.get(opts, :build, [])
request_opts = Keyword.get(opts, :request, [])

method
|> Finch.build(url, headers, body, build_opts)
|> Finch.request(supervisor, request_opts)
|> case do
{:ok, response} -> {:ok, %HTTPResponse{status: response.status, headers: response.headers, body: response.body}}
{:error, error} -> {:error, error}
end
end
end
end
6 changes: 5 additions & 1 deletion lib/assent/http_adapter/mint.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
if Code.ensure_loaded?(Mint.HTTP) do
defmodule Assent.HTTPAdapter.Mint do
@moduledoc """
HTTP adapter module for making http requests with Mint.
Mint can be configured by updating the configuration to
`http_adapter: {HTTPAdapter.Mint, [...]}`.
`http_adapter: {Assent.HTTPAdapter.Mint, [...]}`.
See `Assent.HTTPAdapter` for more.
"""
Expand All @@ -13,6 +14,8 @@ defmodule Assent.HTTPAdapter.Mint do

@impl HTTPAdapter
def request(method, url, body, headers, mint_opts \\ nil) do
IO.warn("#{inspect __MODULE__} is deprecated, consider use #{inspect Assent.HTTPAdapter.Finch} instead")

headers = headers ++ [HTTPAdapter.user_agent_header()]

%{scheme: scheme, port: port, host: host, path: path, query: query} = URI.parse(url)
Expand Down Expand Up @@ -103,3 +106,4 @@ defmodule Assent.HTTPAdapter.Mint do
defp merge_body(_rest, body), do: body
defp merge_body(responses), do: merge_body(responses, "")
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ defmodule Assent.MixProject do
{:certifi, ">= 0.0.0", optional: true},
{:ssl_verify_fun, ">= 0.0.0", optional: true},

{:finch, "~> 0.15", optional: true},
{:mint, "~> 1.0", optional: true},

{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
Expand Down
4 changes: 4 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
%{
"bandit": {:hex, :bandit, "0.7.7", "48456d09022607a312cf723a91992236aeaffe4af50615e6e2d2e383fb6bef10", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 0.6.7", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "772f0a32632c2ce41026d85e24b13a469151bb8cea1891e597fb38fde103640a"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"certifi": {:hex, :certifi, "2.11.0", "5adfe37ceb8569d019f836944aeaf27f8ac391dacaf3707f570c155b7e03aaa8", [:rebar3], [], "hexpm", "9e37e0542ec3fabaa19a0734b3900dc095797fac48c40a2a9741d8ad5e3c9bb7"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
Expand All @@ -14,7 +16,9 @@
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
Expand Down
107 changes: 107 additions & 0 deletions test/assent/http_adapter/finch_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
defmodule Assent.HTTPAdapter.FinchTest do
use ExUnit.Case
doctest Assent.HTTPAdapter.Finch

alias Assent.HTTPAdapter.Finch, as: FinchAdapter
alias Assent.HTTPAdapter.HTTPResponse
alias Finch.Error

import ExUnit.CaptureLog

describe "request/4" do
test "without supervisor" do
TestServer.start(scheme: :https)

assert_raise RuntimeError, "Missing `:supervisor` option for the Assent.HTTPAdapter.Finch configuration", fn ->
FinchAdapter.request(:get, TestServer.url(), nil, [])
end
end

test "handles HTTP/1" do
TestServer.add("/", via: :get)

supervisor = start_supervised_finch!(protocol: :http1)

assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = FinchAdapter.request(:get, TestServer.url(), nil, [], supervisor: supervisor)
end

test "handles SSL" do
TestServer.start(scheme: :https)
TestServer.add("/", via: :get)

supervisor = start_supervised_finch!(conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]])

assert {:ok, %HTTPResponse{status: 200, body: "HTTP/2"}} = FinchAdapter.request(:get, TestServer.url(), nil, [], supervisor: supervisor)
end

test "handles SSL with bad certificate" do
TestServer.start(scheme: :https)

supervisor = start_supervised_finch!(conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]])

bad_host_url = TestServer.url(host: "bad-host.localhost")

assert capture_log(fn ->
assert {:error, %Error{reason: :disconnected}} = FinchAdapter.request(:get, bad_host_url, nil, [], supervisor: supervisor)
end) =~ "{bad_cert,hostname_check_failed}"
end

test "handles SSL with bad certificate and no verification" do
TestServer.start(scheme: :https)
TestServer.add("/", via: :get)

supervisor = start_supervised_finch!(conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none]])

bad_host_url = TestServer.url(host: "bad-host.localhost")

assert {:ok, %HTTPResponse{status: 200}} = FinchAdapter.request(:get, bad_host_url, nil, [], supervisor: supervisor)
end

test "handles unreachable host" do
TestServer.start()
url = TestServer.url()
TestServer.stop()

supervisor = start_supervised_finch!()

assert capture_log(fn ->
assert {:error, %Error{reason: :disconnected}} = FinchAdapter.request(:get, url, nil, [], supervisor: supervisor)
end) =~ "connection refused"
end

test "handles query in URL" do
TestServer.add("/get", via: :get, to: fn conn ->
assert conn.query_string == "a=1"

Plug.Conn.send_resp(conn, 200, "")
end)

supervisor = start_supervised_finch!()

assert {:ok, %HTTPResponse{status: 200}} = FinchAdapter.request(:get, TestServer.url("/get?a=1"), nil, [], supervisor: supervisor)
end

test "handles POST" do
TestServer.add("/post", via: :post, to: fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn, [])
params = URI.decode_query(body)

assert params["a"] == "1"
assert params["b"] == "2"
assert Plug.Conn.get_req_header(conn, "content-type") == ["application/x-www-form-urlencoded"]

Plug.Conn.send_resp(conn, 200, "")
end)

supervisor = start_supervised_finch!()

assert {:ok, %HTTPResponse{status: 200}} = FinchAdapter.request(:post, TestServer.url("/post"), "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}], supervisor: supervisor)
end
end

defp start_supervised_finch!(opts \\ []) do
start_supervised!({Finch, name: FinchTest, pools: %{:default => Keyword.put_new(opts, :protocol, :http2)}})

FinchTest
end
end
29 changes: 22 additions & 7 deletions test/assent/http_adapter/mint_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Assent.HTTPAdapter.MintTest do
use ExUnit.Case
doctest Assent.HTTPAdapter.Mint

alias ExUnit.CaptureIO
alias Mint.TransportError
alias Assent.HTTPAdapter.{HTTPResponse, Mint}

Expand All @@ -12,7 +13,9 @@ defmodule Assent.HTTPAdapter.MintTest do

mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts], protocols: [:http1]]

assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = Mint.request(:get, TestServer.url(), nil, [], mint_opts)
assert CaptureIO.capture_io(:stderr, fn ->
assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = Mint.request(:get, TestServer.url(), nil, [], mint_opts)
end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead"
end

test "handles SSL with bad certificate" do
Expand All @@ -21,7 +24,9 @@ defmodule Assent.HTTPAdapter.MintTest do
bad_host_url = TestServer.url(host: "bad-host.localhost")
mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts]]

assert {:error, %TransportError{reason: {:tls_alert, {:handshake_failure, _error}}}} = Mint.request(:get, bad_host_url, nil, [], mint_opts)
assert CaptureIO.capture_io(:stderr, fn ->
assert {:error, %TransportError{reason: {:tls_alert, {:handshake_failure, _error}}}} = Mint.request(:get, bad_host_url, nil, [], mint_opts)
end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead"
end

test "handles SSL with bad certificate and no verification" do
Expand All @@ -31,7 +36,9 @@ defmodule Assent.HTTPAdapter.MintTest do
bad_host_url = TestServer.url(host: "bad-host.localhost")
mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none]]

assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:get, bad_host_url, nil, [], mint_opts)
assert CaptureIO.capture_io(:stderr, fn ->
assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:get, bad_host_url, nil, [], mint_opts)
end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead"
end

if :crypto.supports()[:curves] do
Expand All @@ -41,7 +48,9 @@ defmodule Assent.HTTPAdapter.MintTest do

mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts]]

assert {:ok, %HTTPResponse{status: 200, body: "HTTP/2"}} = Mint.request(:get, TestServer.url(), nil, [], mint_opts)
assert CaptureIO.capture_io(:stderr, fn ->
assert {:ok, %HTTPResponse{status: 200, body: "HTTP/2"}} = Mint.request(:get, TestServer.url(), nil, [], mint_opts)
end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead"
end
else
IO.warn("No support curve algorithms, can't test in #{__MODULE__}")
Expand All @@ -52,7 +61,9 @@ defmodule Assent.HTTPAdapter.MintTest do
url = TestServer.url()
TestServer.stop()

assert {:error, %TransportError{reason: :econnrefused}} = Mint.request(:get, url, nil, [])
assert CaptureIO.capture_io(:stderr, fn ->
assert {:error, %TransportError{reason: :econnrefused}} = Mint.request(:get, url, nil, [])
end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead"
end

test "handles query in URL" do
Expand All @@ -62,7 +73,9 @@ defmodule Assent.HTTPAdapter.MintTest do
Plug.Conn.send_resp(conn, 200, "")
end)

assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:get, TestServer.url("/get?a=1"), nil, [])
assert CaptureIO.capture_io(:stderr, fn ->
assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:get, TestServer.url("/get?a=1"), nil, [])
end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead"
end

test "handles POST" do
Expand All @@ -77,7 +90,9 @@ defmodule Assent.HTTPAdapter.MintTest do
Plug.Conn.send_resp(conn, 200, "")
end)

assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:post, TestServer.url("/post"), "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}])
assert CaptureIO.capture_io(:stderr, fn ->
assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:post, TestServer.url("/post"), "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}])
end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead"
end
end
end

0 comments on commit c65759d

Please sign in to comment.