From 7faef2dd9941a6a9b4818f3cbb33f9a5c42bd928 Mon Sep 17 00:00:00 2001 From: Olafur Arason Date: Tue, 15 Nov 2022 23:54:18 +0100 Subject: [PATCH] Initial support for tesla to replace hackney There are a lot of http client and a lot of ways of handling requests. Using tesla we can allow users to pick what http client they want and what configurations they want. --- README.md | 23 +++++++++++++++++- config/config.exs | 7 +++++- lib/oauth2/request.ex | 47 +++++++++++++++++-------------------- mix.exs | 5 ++-- mix.lock | 1 + test/oauth2/client_test.exs | 3 --- test/test_helper.exs | 1 + 7 files changed, 55 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index ca01c41..b62e896 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ An Elixir [OAuth](https://en.wikipedia.org/wiki/OAuth) 2.0 Client Library. defp deps do # Add the dependency [ - {:oauth2, "~> 2.0"} + {:oauth2, "~> 2.0"}, + {:hackney, "~> 2.0"} # depending on what tesla adapter you use ] end ``` @@ -47,6 +48,26 @@ end Please see the documentation for [OAuth2.Serializer](https://hexdocs.pm/oauth2/OAuth2.Serializer.html) for more details. +## Configure a http client + +The http client library used is [tesla](https://github.com/elixir-tesla/tesla), the default adapter is +Httpc, since it comes out of the box with every Erlang instance but you can easily change it to something +better. +You can configure another adaptor like this: + +```elixir +config :oauth2, adapter: Tesla.Adapter.Mint +``` + +You can also add your own tesla middleware: + +```elixir +config :oauth2, middleware: [ + Tesla.Middleware.Retry, + {Tesla.Middleware.Fuse, name: :example} +] +``` + ## Debug mode Sometimes it's handy to see what's coming back from the response when getting diff --git a/config/config.exs b/config/config.exs index c7019ac..5e45819 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,4 +8,9 @@ config :oauth2, # second commit sha client_secret: "f715d64092fe81c396ac383e97f8a7eca40e7c89", redirect_uri: "http://example.com/auth/callback", - request_opts: [] + request_opts: [], + middleware: [] + +if Mix.env() == :test do + config :oauth2, adapter: Tesla.Adapter.Hackney +end diff --git a/lib/oauth2/request.ex b/lib/oauth2/request.ex index e672e0b..786ff0c 100644 --- a/lib/oauth2/request.ex +++ b/lib/oauth2/request.ex @@ -14,13 +14,14 @@ defmodule OAuth2.Request do @spec request(atom, Client.t(), binary, body, Client.headers(), Keyword.t()) :: {:ok, Response.t()} | {:ok, reference} | {:error, Response.t()} | {:error, Error.t()} def request(method, %Client{} = client, url, body, headers, opts) do - url = client |> process_url(url) |> process_params(opts[:params]) + url = process_url(client, url) headers = req_headers(client, headers) |> normalize_headers() |> Enum.uniq() content_type = content_type(headers) serializer = Client.get_serializer(client, content_type) body = encode_request_body(body, content_type, serializer) headers = process_request_headers(headers, content_type) req_opts = Keyword.merge(client.request_opts, opts) + params = opts[:params] || %{} if Application.get_env(:oauth2, :debug) do Logger.debug(""" @@ -33,16 +34,20 @@ defmodule OAuth2.Request do """) end - case :hackney.request(method, url, headers, body, req_opts) do - {:ok, ref} when is_reference(ref) -> - {:ok, ref} - - {:ok, status, headers, ref} when is_reference(ref) -> - process_body(client, status, headers, ref) - - {:ok, status, headers, body} when is_binary(body) -> + case Tesla.request(http_client(), + method: method, + url: url, + query: params, + headers: headers, + body: body, + opts: [adapter: req_opts] + ) do + {:ok, %{status: status, headers: headers, body: body}} when is_binary(body) -> process_body(client, status, headers, body) + {:ok, %{body: ref}} when is_reference(ref) -> + {:ok, ref} + {:error, reason} -> {:error, %Error{reason: reason}} end @@ -80,6 +85,14 @@ defmodule OAuth2.Request do end end + defp http_client do + adapter = Application.get_env(:oauth2, :adapter, Tesla.Adapter.Httpc) + + middleware = Application.get_env(:oauth2, :middleware, []) + + Tesla.client(middleware, adapter) + end + defp process_url(client, url) do case String.downcase(url) do <<"http://"::utf8, _::binary>> -> url @@ -88,16 +101,6 @@ defmodule OAuth2.Request do end end - defp process_body(client, status, headers, ref) when is_reference(ref) do - case :hackney.body(ref) do - {:ok, body} -> - process_body(client, status, headers, body) - - {:error, reason} -> - {:error, %Error{reason: reason}} - end - end - defp process_body(client, status, headers, body) when is_binary(body) do resp = Response.new(client, status, headers, body) @@ -110,12 +113,6 @@ defmodule OAuth2.Request do end end - defp process_params(url, nil), - do: url - - defp process_params(url, params), - do: url <> "?" <> URI.encode_query(params) - defp req_headers(%Client{token: nil} = client, headers), do: headers ++ client.headers diff --git a/mix.exs b/mix.exs index 98d92fe..60e282b 100644 --- a/mix.exs +++ b/mix.exs @@ -27,7 +27,7 @@ defmodule OAuth2.Mixfile do end def application do - [applications: [:logger, :hackney]] + [extra_applications: [:logger]] end defp dialyzer do @@ -38,9 +38,10 @@ defmodule OAuth2.Mixfile do defp deps do [ - {:hackney, "~> 1.13"}, + {:tesla, "~> 1.4"}, # Test dependencies + {:hackney, "~> 1.17", only: [:dev, :test]}, {:jason, "~> 1.0", only: [:dev, :test]}, {:bypass, "~> 0.9", only: :test}, {:plug_cowboy, "~> 1.0", only: :test}, diff --git a/mix.lock b/mix.lock index 6cd5105..af29a49 100644 --- a/mix.lock +++ b/mix.lock @@ -29,5 +29,6 @@ "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm", "6e56493a862433fccc3aca3025c946d6720d8eedf6e3e6fb911952a7071c357f"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/oauth2/client_test.exs b/test/oauth2/client_test.exs index 01c3dc4..33f1fd8 100644 --- a/test/oauth2/client_test.exs +++ b/test/oauth2/client_test.exs @@ -200,9 +200,6 @@ defmodule OAuth2.ClientTest do {:ok, ref} = Client.get(client, "/api/user/1") - assert_receive {:hackney_response, ^ref, {:status, 200, "OK"}} - assert_receive {:hackney_response, ^ref, {:headers, headers}} - assert {_, "8000"} = List.keyfind(headers, "content-length", 0) resp_body = stream(ref) assert resp_body == body end diff --git a/test/test_helper.exs b/test/test_helper.exs index 98fd96f..cec051b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,4 @@ Application.ensure_all_started(:bypass) +Application.ensure_all_started(:hackney) Application.put_env(:oauth2, :warn_missing_serializer, false) ExUnit.start()