Skip to content

Commit

Permalink
Add req http adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
danschultzer committed Nov 19, 2023
1 parent 0a1a5eb commit 5bbd5ee
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 31 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## v0.2.8 (TBA)

`Req` will be used by default if available in your project, otherwise `:httpc` will be used.

- `Req` HTTP adapter added
- `Req` supported by default as HTTP client
- Global application config support for HTTP and JWT adapters
- More expressive errors now including the whole HTTP response where applicable

## v0.2.7 (2023-09-12)
Expand Down
85 changes: 74 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,31 @@ Add Assent to your list of dependencies in `mix.exs`:
defp deps do
[
# ...
{:assent, "~> 0.2.7"},

# Required for SSL validation with :httpc adapter
{:certifi, "~> 2.4"},
{:ssl_verify_fun, "~> 1.1"}
{:assent, "~> 0.2.7"}
]
end
```

Run `mix deps.get` to install it.

### Releases
#### HTTP client installation

By default, `Req` is used if you have it in your dependency list. If not, Erlang's `:httpc` will be used instead.

By default, `:httpc` will be used for HTTP requests. To compile the app with `:httpc` support, please add `:inets` to `:extra_applications` in `mix.exs`:
If you are using `:httpc` you should add the following dependencies to enable SSL validation:

```elixir
defp deps do
[
# ...
# Required for SSL validation when using the `:httpc` adapter
{:certifi, "~> 2.4"},
{:ssl_verify_fun, "~> 1.1"}
]
end
```

You must also add `:inets` to `:extra_applications` in `mix.exs`:

```elixir
def application do
Expand All @@ -68,7 +79,7 @@ def application do
end
```

This is not necessary if you use another HTTP adapter like Finch.
This is not necessary if you use another HTTP adapter like `Req` or `Finch`.

## Getting started

Expand Down Expand Up @@ -241,14 +252,60 @@ defmodule TestProvider do
end
```

## HTTP Adapter
## HTTP Client

Assent supports [`Req`](https://github.com/wojtekmach/req), [`Finch`](https://github.com/sneako/finch), and [`:httpc`](https://www.erlang.org/doc/man/httpc.html) out of the box. The `Req` HTTP client adapter will be used by default if enabled, otherwise Erlang's `:httpc` adapter will be included.

You can explicitly set the HTTP client adapter in the configuration:

```elixir
config = [
client_id: "REPLACE_WITH_CLIENT_ID",
client_secret: "REPLACE_WITH_CLIENT_SECRET",
http_adapter: Assent.HTTPAdapter.Httpc
]
```

Or globally in the config:

```elixir
config :assent, http_adapter: Assent.HTTPAdapter.Httpc
```

### `Req`

Req doesn't require any additional configuration and will work out of the box:

```elixir
defp deps do
[
# ...
{:req, "~> 0.4"}
]
end
```

### `:httpc`

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 `Req` is not available, 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 [`Finch`](https://github.com/sneako/finch) to your project.
```elixir
defp deps do
[
# ...
# Required for SSL validation if using the `:httpc` adapter
{:certifi, "~> 2.4"},
{:ssl_verify_fun, "~> 1.1"}
]
end
```

You must include `:inets` to `:extra_applications` to include `:httpc` in your release.

### Finch

Finch will require a supervisor in your application.

Update `mix.exs`:

```elixir
Expand Down Expand Up @@ -295,6 +352,12 @@ config = [
]
```

Or globally in the config:

```elixir
config :assent, jwt_adapter: AssAssent.JWTAdapter.JOSE
```

## LICENSE

(The MIT License)
Expand Down
13 changes: 13 additions & 0 deletions lib/assent/http_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ defmodule Assent.HTTPAdapter do
@moduledoc """
HTTP adapter helper module.
You can configure the which HTTP adapter Assent uses by setting the
configuring:
http_adapter: Assent.HTTPAdapter.Httpc
Default options can be set by passing a list of options:
http_adapter: {Assent.HTTPAdapter.Httpc, [...]}
You can also set global application config:
config :assent, :http_adapter, Assent.HTTPAdapter.Httpc
## Usage
defmodule MyApp.MyHTTPAdapter do
Expand Down
33 changes: 29 additions & 4 deletions lib/assent/http_adapter/httpc.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
defmodule Assent.HTTPAdapter.Httpc do
@moduledoc """
HTTP adapter module for making http requests with httpc.
HTTP adapter module for making http requests with `:httpc`.
SSL support will automatically be enabled if the `:certifi` and
`:ssl_verify_fun` libraries exists in your project. You can also override
the httpc options by updating the configuration:
the `:httpc` options by updating the configuration:
http_adapter: {Assent.HTTPAdapter.Httpc, [...]}
For releases please make sure you have included `:inets` in your application:
extra_applications: [:inets]
See `Assent.HTTPAdapter` for more.
"""
alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse}
Expand All @@ -16,6 +20,8 @@ defmodule Assent.HTTPAdapter.Httpc do

@impl HTTPAdapter
def request(method, url, body, headers, httpc_opts \\ nil) do
raise_on_missing_httpc!()

headers = headers ++ [HTTPAdapter.user_agent_header()]
request = httpc_request(url, body, headers)
opts = parse_httpc_ssl_opts(httpc_opts, url)
Expand All @@ -25,6 +31,25 @@ defmodule Assent.HTTPAdapter.Httpc do
|> format_response()
end

defp raise_on_missing_httpc! do
Code.ensure_loaded?(:httpc) || raise """
#{inspect __MODULE__} requires `:httpc` to be included in your
application.
Please add `:inets` to `:extra_applications`:
def application do
[
# ...
extra_applications: [
#...
:inets
]
]
end
"""
end

defp httpc_request(url, body, headers) do
url = to_charlist(url)
headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
Expand Down Expand Up @@ -103,7 +128,7 @@ defmodule Assent.HTTPAdapter.Httpc do

defp raise_on_missing_ssl_verify_fun! do
Code.ensure_loaded?(:ssl_verify_hostname) || raise """
This request can NOT be verified for valid SSL certificate.
This #{inspect __MODULE__} request can NOT be verified for valid SSL certificate.
Please add `:ssl_verify_fun` to your projects dependencies:
Expand Down Expand Up @@ -134,7 +159,7 @@ defmodule Assent.HTTPAdapter.Httpc do

defp raise_on_missing_certifi! do
Code.ensure_loaded?(:certifi) || raise """
This request requires a CA trust store.
This #{inspect __MODULE__} request requires a CA trust store.
Please add `:certifi` to your projects dependencies:
Expand Down
10 changes: 1 addition & 9 deletions lib/assent/http_adapter/mint.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
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:
http_adapter: {Assent.HTTPAdapter.Mint, [...]}
See `Assent.HTTPAdapter` for more.
"""
@moduledoc false
alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse}

@behaviour HTTPAdapter
Expand Down
43 changes: 43 additions & 0 deletions lib/assent/http_adapter/req.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
if Code.ensure_loaded?(Req) do
defmodule Assent.HTTPAdapter.Req do
@moduledoc """
HTTP adapter module for making http requests with Req.
You can also override the Req options by updating the configuration:
http_adapter: {Assent.HTTPAdapter.Req, [...]}
See `Assent.HTTPAdapter` for more.
"""
alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse}

@behaviour HTTPAdapter

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

opts =
Keyword.merge([
method: method,
url: url,
headers: headers,
body: body
], opts)

opts
|> Req.new()
|> Req.request()
|> case do
{:ok, response} ->
headers = Enum.map(headers, fn {key, value} -> {String.downcase(to_string(key)), to_string(value)} end)

{:ok, %HTTPResponse{status: response.status, headers: headers, body: response.body}}

{:error, error} ->
{:error, error}
end
end
end
end
19 changes: 16 additions & 3 deletions lib/assent/jwt_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ defmodule Assent.JWTAdapter do
@moduledoc """
JWT adapter helper module.
You can configure the JWT adapter by updating the configuration:
jwt_adapter: {Assent.JWTAdapter.AssentJWT, [...]}
Default options can be set by passing a list of options:
jwt_adapter: {Assent.JWTAdapter.AssentJWT, [...]}
You can also set global application config:
config :assent, :jwt_adapter, Assent.JWTAdapter.AssentJWT
## Usage
defmodule MyApp.MyJWTAdapter do
Expand Down Expand Up @@ -44,10 +56,11 @@ defmodule Assent.JWTAdapter do

defp fetch_adapter(opts) do
default_opts = Keyword.put(opts, :json_library, Config.json_library(opts))
default_jwt_adapter = Application.get_env(:assent, :jwt_adapter, Assent.JWTAdapter.AssentJWT)

case Keyword.get(opts, :jwt_adapter, Assent.JWTAdapter.AssentJWT) do
{adapter, opts} -> {adapter, Keyword.merge(default_opts, opts)}
adapter -> {adapter, default_opts}
case Keyword.get(opts, :jwt_adapter, default_jwt_adapter) do
{adapter, opts} -> {adapter, Keyword.merge(default_opts, opts)}
adapter when is_atom(adapter) -> {adapter, default_opts}
end
end

Expand Down
4 changes: 4 additions & 0 deletions lib/assent/jwt_adapter/assent_jwt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ defmodule Assent.JWTAdapter.AssentJWT do
@moduledoc """
JWT adapter module for parsing JSON Web Tokens natively.
You can append options to the configuration:
jwt_adapter: {Assent.JWTAdapter.AssentJWT, [...]}
See `Assent.JWTAdapter` for more.
"""
alias Assent.{Config, JWTAdapter}
Expand Down
4 changes: 4 additions & 0 deletions lib/assent/jwt_adapter/jose.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ defmodule Assent.JWTAdapter.JOSE do
@moduledoc """
JWT adapter module for parsing JSON Web Tokens with JOSE.
You can append options to the configuration:
jwt_adapter: {Assent.JWTAdapter.JOSE, [...]}
See `Assent.JWTAdapter` for more.
"""
alias Assent.JWTAdapter
Expand Down
16 changes: 13 additions & 3 deletions lib/assent/strategy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,20 @@ defmodule Assent.Strategy do
|> decode_response(config)
end

@default_http_client Enum.find_value([
{Req1, Assent.HTTPAdapter.Req},
{:httpc, Assent.HTTPAdapter.Httpc}
],
fn {dep, module} ->
Code.ensure_loaded?(dep) && {module, []}
end)

defp fetch_http_adapter(config) do
case Config.get(config, :http_adapter, Assent.HTTPAdapter.Httpc) do
{http_adapter, opts} -> {http_adapter, opts}
http_adapter -> {http_adapter, nil}
default_http_adapter = Application.get_env(:assent, :http_adapter, @default_http_client)

case Config.get(config, :http_adapter, default_http_adapter) do
{http_adapter, opts} -> {http_adapter, opts}
http_adapter when is_atom(http_adapter) -> {http_adapter, nil}
end
end

Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ defmodule Assent.MixProject do

{:finch, "~> 0.15", optional: true},
{:mint, "~> 1.0", optional: true},
{:req, "~> 0.4", optional: true},
{:jason, "~> 1.0", optional: true}, # Required for Req

{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},

{:credo, "~> 1.1", only: [:dev, :test]},
{:jason, "~> 1.0", only: [:dev, :test]},
{:test_server, "~> 0.1.0", only: :test},
{:bandit, ">= 0.0.0", only: :test}
]
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"req": {:hex, :req, "0.4.5", "2071bbedd280f107b9e33e1ddff2beb3991ec1ae06caa2cca2ab756393d8aca5", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dd23e9c7303ddeb2dee09ff11ad8102cca019e38394456f265fb7b9655c64dd8"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"test_server": {:hex, :test_server, "0.1.14", "c3cdf0b6c1be691ae50a14ee3ea4bd026250c321c2012f5dfaed336d8702a562", [:mix], [{:bandit, ">= 0.7.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 2.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:x509, "~> 0.6", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "5af2f98a22765ff3cc66f09d20c88b754586cf0c45cf0d2e0068e2a47f5041a7"},
Expand Down
Loading

0 comments on commit 5bbd5ee

Please sign in to comment.