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

Mint adapter #297

Merged
merged 16 commits into from
Aug 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ if Mix.env() == :test do
sasl_error_logger: false

config :tesla, MockClient, adapter: Tesla.Mock

config :tesla, Tesla.Adapter.Mint,
cacert: ["./deps/httparrot/priv/ssl/server-ca.crt"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericmj This config won't end up in production builds, right? I mean, it will be nil by default so it should be safe to reference httparrot here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be nil by default.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, config.exs is not used from dependencies. So the Tesla.Adapter.Mint key will not be set.

end
190 changes: 190 additions & 0 deletions lib/tesla/adapter/mint.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
defmodule Tesla.Adapter.Mint do
@moduledoc """
Adapter for [mint](https://github.com/ericmj/mint)

Caution: The minimum supported Elixir version for mint is 1.5.0

Remember to add `{:mint, "~> 0.2.0"}` and `{:castore, "~> 0.1.0"}` to dependencies
Also, you need to recompile tesla after adding `:mint` dependency:

```
mix deps.clean tesla
mix deps.compile tesla
```

### Example usage
```
# set globally in config/config.exs
config :tesla, :adapter, Tesla.Adapter.Mint

# set per module
defmodule MyClient do
use Tesla

adapter Tesla.Adapter.Mint
end

# set global custom cacert
config :tesla, Tesla.Adapter.Mint, cacert: ["path_to_cacert"]
"""
@behaviour Tesla.Adapter
import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1]
alias Tesla.Multipart
alias Mint.HTTP

@default adapter: [timeout: 2_000]

@doc false
def call(env, opts) do
opts = Tesla.Adapter.opts(@default, env, opts)

with {:ok, status, headers, body} <- request(env, opts) do
{:ok, %{env | status: status, headers: headers, body: body}}
end
end

defp request(env, opts) do
# Break the URI
%URI{host: host, scheme: scheme, port: port, path: path, query: query} = URI.parse(env.url)
query = (query || "") |> URI.decode_query() |> Map.to_list()
path = Tesla.build_url(path, env.query ++ query)

method = env.method |> Atom.to_string() |> String.upcase()

# Set the global cacert file
opts =
if scheme == "https" && !is_nil(get_global_default_ca()) do
transport_opts = Access.get(opts, :transport_opts, [])

transport_opts =
Keyword.put(
transport_opts,
:cacertfile,
Keyword.get(transport_opts, :cacertfile, []) ++ get_global_default_ca()
)

Keyword.put(opts, :transport_opts, transport_opts)
else
opts
end

request(
method,
scheme,
host,
port,
path,
env.headers,
env.body,
opts
)
end

defp request(method, scheme, host, port, path, headers, %Stream{} = body, opts) do
fun = stream_to_fun(body)
request(method, scheme, host, port, path, headers, fun, opts)
end

defp request(method, scheme, host, port, path, headers, %Multipart{} = body, opts) do
headers = headers ++ Multipart.headers(body)
fun = stream_to_fun(Multipart.body(body))
request(method, scheme, host, port, path, headers, fun, opts)
end

defp request(method, scheme, host, port, path, headers, body, opts) when is_function(body) do
with {:ok, conn} <- HTTP.connect(String.to_atom(scheme), host, port, opts),
# FIXME Stream function in Mint will not append the content length after eof
# This will trigger the failure in unit test
{:ok, body, length} <- stream_request(body),
{:ok, conn, _req_ref} <-
HTTP.request(
conn,
method,
path || "/",
headers ++ [{"content-length", "#{length}"}],
body
),
{:ok, conn, res = %{status: status, headers: headers}} <- stream_response(conn, opts),
{:ok, _conn} <- HTTP.close(conn) do
{:ok, status, headers, Map.get(res, :data)}
end
end

defp request(method, scheme, host, port, path, headers, body, opts) do
with {:ok, conn} <- HTTP.connect(String.to_atom(scheme), host, port, opts),
{:ok, conn, _req_ref} <- HTTP.request(conn, method, path || "/", headers, body),
{:ok, conn, res = %{status: status, headers: headers}} <- stream_response(conn, opts),
{:ok, _conn} <- HTTP.close(conn) do
{:ok, status, headers, Map.get(res, :data)}
end
end

defp get_global_default_ca() do
case Application.get_env(:tesla, Tesla.Adapter.Mint) do
nil -> nil
env -> Keyword.get(env, :cacert)
end
RyanSiu1995 marked this conversation as resolved.
Show resolved Hide resolved
end

defp stream_request(fun, body \\ "") do
case next_chunk(fun) do
{:ok, item, fun} when is_list(item) ->
stream_request(fun, body <> List.to_string(item))

{:ok, item, fun} ->
stream_request(fun, body <> item)

:eof ->
{:ok, body, byte_size(body)}
end
end

defp stream_response(conn, opts, response \\ %{}) do
receive do
RyanSiu1995 marked this conversation as resolved.
Show resolved Hide resolved
msg ->
case HTTP.stream(conn, msg) do
{:ok, conn, stream} ->
response =
Enum.reduce(stream, response, fn
{:status, _req_ref, code}, acc ->
Map.put(acc, :status, code)

{:headers, _req_ref, headers}, acc ->
Map.put(acc, :headers, Map.get(acc, :headers, []) ++ headers)

{:data, _req_ref, data}, acc ->
Map.put(acc, :data, Map.get(acc, :data, "") <> data)

{:done, _req_ref}, acc ->
Map.put(acc, :done, true)

{:error, _req_ref, reason}, acc ->
Map.put(acc, :error, reason)

_, acc ->
acc
end)

cond do
Map.has_key?(response, :error) ->
{:error, Map.get(response, :error)}

Map.has_key?(response, :done) ->
{:ok, conn, Map.drop(response, [:done])}

true ->
stream_response(conn, opts, response)
end

{:error, _conn, error, _res} ->
{:error, "Encounter Mint error #{inspect(error)}"}

:unknown ->
{:error, "Encounter unknown error"}
end
after
opts |> Keyword.get(:adapter) |> Keyword.get(:timeout) ->
{:error, "Response timeout"}
end
end
end
7 changes: 5 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule Tesla.Mixfile do
package: package(),
source_ref: "v#{@version}",
source_url: "https://github.com/teamon/tesla",
elixir: "~> 1.4",
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env()),
deps: deps(),
lockfile: lockfile(System.get_env("LOCKFILE")),
Expand Down Expand Up @@ -61,6 +61,8 @@ defmodule Tesla.Mixfile do
{:ibrowse, "~> 4.4.0", optional: true},
{:hackney, "~> 1.6", optional: true},
{:gun, "~> 1.3", optional: true},
{:castore, "~> 0.1.0", optional: true},
{:mint, "~> 0.2.0", optional: true},

# json parsers
{:jason, ">= 1.0.0", optional: true},
Expand Down Expand Up @@ -94,7 +96,8 @@ defmodule Tesla.Mixfile do
Tesla.Adapter.Hackney,
Tesla.Adapter.Httpc,
Tesla.Adapter.Ibrowse,
Tesla.Adapter.Gun
Tesla.Adapter.Gun,
Tesla.Adapter.Mint
],
Middlewares: [
Tesla.Middleware.BaseUrl,
Expand Down
15 changes: 8 additions & 7 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
%{
"castore": {:hex, :castore, "0.1.2", "81adb0683c4ec8ebb97ad777ec1b050405282d55453df14567a3c73ae25932a6", [:mix], [], "hexpm"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"con_cache": {:hex, :con_cache, "0.13.1", "047e097ab2a8c6876e12d0c29e29a86d487b592df97b98e3e2abedad574e215d", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "2.5.0", "4ef3ae066ee10fe01ea3272edc8f024347a0d3eb95f6fbb9aed556dacbfc1337", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.6.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
"erlex": {:hex, :erlex, "0.2.2", "cb0e6878fdf86dc63509eaf2233a71fa73fc383c8362c8ff8e8b6f0c2bb7017c", [:mix], [], "hexpm"},
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "83c5440c261961387ce21bf6760729ee7f9ba052", []},
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.4", "52aba89c60529284df5fc18adc4c808b7346e72668bc2fb2b68d7394996c4af8", [:mix], [], "hexpm"},
"erlex": {:hex, :erlex, "0.2.4", "23791959df45fe8f01f388c6f7eb733cc361668cbeedd801bf491c55a029917b", [:mix], [], "hexpm"},
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "bc1fd8a0b75d934d6fbad7e4600ad7bc0e3f71dc", []},
"excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
"fuse": {:hex, :fuse, "2.4.2", "9106b08db8793a34cc156177d7e24c41bd638ee1b28463cb76562fde213e8ced", [:rebar3], [], "hexpm"},
"gun": {:hex, :gun, "1.3.0", "18e5d269649c987af95aec309f68a27ffc3930531dd227a6eaa0884d6684286e", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httparrot": {:hex, :httparrot, "1.2.0", "5ca2cb7aa936e8f418051b615fb8ec419ec7f29e792ae9fb698393e82513457b", [:mix], [{:con_cache, "~> 0.13.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:cowboy, "~> 2.5.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:exjsx, "~> 3.0 or ~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}], "hexpm"},
"ibrowse": {:hex, :ibrowse, "4.4.0", "2d923325efe0d2cb09b9c6a047b2835a5eda69d8a47ed6ff8bc03628b764e991", [:rebar3], [], "hexpm"},
"ibrowse": {:hex, :ibrowse, "4.4.1", "2b7d0637b0f8b9b4182de4bd0f2e826a4da2c9b04898b6e15659ba921a8d6ec2", [:rebar3], [], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"},
"makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
"mint": {:hex, :mint, "0.2.1", "a2ec8729fcad5c8b6460e07dfa64b008b3d9697a9f4604cd5684a87b44677c99", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm"},
"mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
Expand Down
18 changes: 18 additions & 0 deletions test/tesla/adapter/mint_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule Tesla.Adapter.MintTest do
use ExUnit.Case

use Tesla.AdapterCase, adapter: Tesla.Adapter.Mint
use Tesla.AdapterCase.Basic
use Tesla.AdapterCase.Multipart
use Tesla.AdapterCase.StreamRequestBody
use Tesla.AdapterCase.SSL

test "Delay request" do
request = %Env{
method: :head,
url: "#{@http}/delay/1"
}

assert {:error, "Response timeout"} = call(request, adapter: [timeout: 100])
end
end