From 2acabfa6e100974cb2d6c716aec355e712056080 Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Wed, 8 May 2019 15:50:51 +0800 Subject: [PATCH 01/16] Setup happy path logic for adapter mint --- lib/tesla/adapter/mint.ex | 75 +++++++++++++++++++++++++++++++++++++++ mix.exs | 2 ++ mix.lock | 15 ++++---- 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 lib/tesla/adapter/mint.ex diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex new file mode 100644 index 00000000..47836115 --- /dev/null +++ b/lib/tesla/adapter/mint.ex @@ -0,0 +1,75 @@ +defmodule Tesla.Adapter.Mint do + @moduledoc false + @behaviour Tesla.Adapter + alias Mint.HTTP + + @doc false + def call(env, opts) do + with {:ok, status, headers, body} <- request(env, opts) do + {:ok, %{env | status: status, headers: format_headers(headers), body: body}} + end + end + + defp format_headers(headers) do + for {key, value} <- headers do + {String.downcase(to_string(key)), to_string(value)} + end + end + + defp request(env, opts) do + %URI{host: host, scheme: scheme, port: port, path: path} = URI.parse(env.url) + request( + env.method |> Atom.to_string() |> String.upcase(), + scheme, + host, + port, + path, + env.headers, + env.body, + opts + ) + end + + defp request(method, scheme, host, port, path, headers, body, opts) do + with {:ok, conn} <- HTTP.connect(String.to_atom(scheme), host, port), + {:ok, conn, _req_ref} <- HTTP.request(conn, method, path || "/", headers, body || ""), + {:ok, _conn, %{status: status, headers: headers, data: body}} <- stream_response(conn) do + {:ok, status, headers, body} + end + end + + defp stream_response(conn, response \\ %{}) do + receive do + msg -> + case HTTP.stream(conn, msg) do + {:ok, conn, stream} -> + response = + Enum.reduce(stream, response, fn x, acc -> + case x do + {:status, _req_ref, code} -> + Map.put(acc, :status, code) + + {:headers, _req_ref, headers} -> + Map.put(acc, :headers, headers) + + {:data, _req_ref, data} -> + Map.put(acc, :data, Map.get(acc, :data, "") <> data) + + {:done, _req_ref} -> + Map.put(acc, :done, true) + end + end) + + if Map.get(response, :done) do + response = Map.drop(response, [:done]) + {:ok, conn, response} + else + stream_response(conn, response) + end + + _ -> + {:error, "TODO: Error Handle"} + end + end + end +end diff --git a/mix.exs b/mix.exs index d0eed777..8c438659 100644 --- a/mix.exs +++ b/mix.exs @@ -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"}, + {:mint, "~> 0.2.0"}, # json parsers {:jason, ">= 1.0.0", optional: true}, diff --git a/mix.lock b/mix.lock index d079db9f..11b26746 100644 --- a/mix.lock +++ b/mix.lock @@ -1,13 +1,13 @@ %{ + "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"}, @@ -15,16 +15,17 @@ "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"}, From 22e5b4449eecb184eae6f40f71b07e5b920e7cac Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Wed, 8 May 2019 17:21:56 +0800 Subject: [PATCH 02/16] Handle stream request body for Adapter Mint --- lib/tesla/adapter/mint.ex | 34 ++++++++++++++++++- .../adapter_case/stream_request_body.ex | 4 +-- test/tesla/adapter/mint_test.exs | 8 +++++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test/tesla/adapter/mint_test.exs diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 47836115..356e6808 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -1,8 +1,10 @@ defmodule Tesla.Adapter.Mint do @moduledoc false @behaviour Tesla.Adapter + import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] alias Mint.HTTP + @doc false def call(env, opts) do with {:ok, status, headers, body} <- request(env, opts) do @@ -18,8 +20,14 @@ defmodule Tesla.Adapter.Mint do defp request(env, opts) do %URI{host: host, scheme: scheme, port: port, path: path} = URI.parse(env.url) + path = Tesla.build_url(path, env.query) + method = case env.method do + :head -> "GET" + m -> m |> Atom.to_string() |> String.upcase() + end + request( - env.method |> Atom.to_string() |> String.upcase(), + method, scheme, host, port, @@ -29,6 +37,20 @@ defmodule Tesla.Adapter.Mint do 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, body, opts) when is_function(body) do + with {:ok, conn} <- HTTP.connect(String.to_atom(scheme), host, port), + {:ok, conn, req_ref} <- HTTP.request(conn, method, path || "/", headers, :stream), + {:ok, conn} <- stream_request(conn, req_ref, body), + {:ok, _conn, %{status: status, headers: headers, data: body}} <- stream_response(conn) do + {:ok, status, headers, body} + end + end defp request(method, scheme, host, port, path, headers, body, opts) do with {:ok, conn} <- HTTP.connect(String.to_atom(scheme), host, port), @@ -38,6 +60,16 @@ defmodule Tesla.Adapter.Mint do end end + defp stream_request(conn, req_ref, fun) do + case next_chunk(fun) do + {:ok, item, fun} -> + HTTP.stream_request_body(conn, req_ref, item) + stream_request(conn, req_ref, fun) + :eof -> + HTTP.stream_request_body(conn, req_ref, :eof) + end + end + defp stream_response(conn, response \\ %{}) do receive do msg -> diff --git a/test/support/adapter_case/stream_request_body.ex b/test/support/adapter_case/stream_request_body.ex index 69cb1c28..7c2cbc8e 100644 --- a/test/support/adapter_case/stream_request_body.ex +++ b/test/support/adapter_case/stream_request_body.ex @@ -8,7 +8,7 @@ defmodule Tesla.AdapterCase.StreamRequestBody do request = %Env{ method: :post, url: "#{@http}/post", - headers: [{"content-type", "text/plain"}], + headers: [{"content-type", "text/plain"}, {"content-length", "5"}], body: Stream.map(1..5, &to_string/1) } @@ -28,7 +28,7 @@ defmodule Tesla.AdapterCase.StreamRequestBody do request = %Env{ method: :post, url: "#{@http}/post", - headers: [{"content-type", "text/plain"}], + headers: [{"content-type", "text/plain"}, {"content-length", "5"}], body: body } diff --git a/test/tesla/adapter/mint_test.exs b/test/tesla/adapter/mint_test.exs new file mode 100644 index 00000000..2c208ace --- /dev/null +++ b/test/tesla/adapter/mint_test.exs @@ -0,0 +1,8 @@ +defmodule Tesla.Adapter.MintTest do + use ExUnit.Case + + use Tesla.AdapterCase, adapter: Tesla.Adapter.Mint + use Tesla.AdapterCase.Basic + use Tesla.AdapterCase.StreamRequestBody + use Tesla.AdapterCase.SSL +end From eca9cf3d31c06a5525f96ab7a0f7bd13d9caf80b Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Wed, 8 May 2019 17:58:47 +0800 Subject: [PATCH 03/16] Fix the URI parsing before request in Adapter Mint --- lib/tesla/adapter/mint.ex | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 356e6808..8395b1b1 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -4,7 +4,6 @@ defmodule Tesla.Adapter.Mint do import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] alias Mint.HTTP - @doc false def call(env, opts) do with {:ok, status, headers, body} <- request(env, opts) do @@ -19,8 +18,10 @@ defmodule Tesla.Adapter.Mint do end defp request(env, opts) do - %URI{host: host, scheme: scheme, port: port, path: path} = URI.parse(env.url) - path = Tesla.build_url(path, env.query) + # 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 = case env.method do :head -> "GET" m -> m |> Atom.to_string() |> String.upcase() @@ -47,16 +48,16 @@ defmodule Tesla.Adapter.Mint do with {:ok, conn} <- HTTP.connect(String.to_atom(scheme), host, port), {:ok, conn, req_ref} <- HTTP.request(conn, method, path || "/", headers, :stream), {:ok, conn} <- stream_request(conn, req_ref, body), - {:ok, _conn, %{status: status, headers: headers, data: body}} <- stream_response(conn) do - {:ok, status, headers, body} + {:ok, _conn, res = %{status: status, headers: headers}} <- stream_response(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), {:ok, conn, _req_ref} <- HTTP.request(conn, method, path || "/", headers, body || ""), - {:ok, _conn, %{status: status, headers: headers, data: body}} <- stream_response(conn) do - {:ok, status, headers, body} + {:ok, _conn, res = %{status: status, headers: headers}} <- stream_response(conn) do + {:ok, status, headers, Map.get(res, :data)} end end From ee6a72df1db4f126b16bc157a9bc997b2793b5b0 Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Wed, 8 May 2019 23:04:16 +0800 Subject: [PATCH 04/16] Fix streaming request body problem and add multipart support for adapter mint --- lib/tesla/adapter/mint.ex | 25 +++++++++++++------ .../adapter_case/stream_request_body.ex | 4 +-- test/tesla/adapter/mint_test.exs | 1 + 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 8395b1b1..cc2eee82 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -2,6 +2,7 @@ defmodule Tesla.Adapter.Mint do @moduledoc false @behaviour Tesla.Adapter import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] + alias Tesla.Multipart alias Mint.HTTP @doc false @@ -44,11 +45,20 @@ defmodule Tesla.Adapter.Mint do 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), - {:ok, conn, req_ref} <- HTTP.request(conn, method, path || "/", headers, :stream), - {:ok, conn} <- stream_request(conn, req_ref, body), - {:ok, _conn, res = %{status: status, headers: headers}} <- stream_response(conn) do + # 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) + do {:ok, status, headers, Map.get(res, :data)} end end @@ -61,13 +71,14 @@ defmodule Tesla.Adapter.Mint do end end - defp stream_request(conn, req_ref, fun) do + 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} -> - HTTP.stream_request_body(conn, req_ref, item) - stream_request(conn, req_ref, fun) + stream_request(fun, body <> item) :eof -> - HTTP.stream_request_body(conn, req_ref, :eof) + {:ok, body, byte_size body} end end diff --git a/test/support/adapter_case/stream_request_body.ex b/test/support/adapter_case/stream_request_body.ex index 7c2cbc8e..69cb1c28 100644 --- a/test/support/adapter_case/stream_request_body.ex +++ b/test/support/adapter_case/stream_request_body.ex @@ -8,7 +8,7 @@ defmodule Tesla.AdapterCase.StreamRequestBody do request = %Env{ method: :post, url: "#{@http}/post", - headers: [{"content-type", "text/plain"}, {"content-length", "5"}], + headers: [{"content-type", "text/plain"}], body: Stream.map(1..5, &to_string/1) } @@ -28,7 +28,7 @@ defmodule Tesla.AdapterCase.StreamRequestBody do request = %Env{ method: :post, url: "#{@http}/post", - headers: [{"content-type", "text/plain"}, {"content-length", "5"}], + headers: [{"content-type", "text/plain"}], body: body } diff --git a/test/tesla/adapter/mint_test.exs b/test/tesla/adapter/mint_test.exs index 2c208ace..5b8f63ba 100644 --- a/test/tesla/adapter/mint_test.exs +++ b/test/tesla/adapter/mint_test.exs @@ -3,6 +3,7 @@ defmodule Tesla.Adapter.MintTest do use Tesla.AdapterCase, adapter: Tesla.Adapter.Mint use Tesla.AdapterCase.Basic + use Tesla.AdapterCase.Multipart use Tesla.AdapterCase.StreamRequestBody use Tesla.AdapterCase.SSL end From b2117e914d22d75d9646899cbd66b24f55356ed9 Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Fri, 10 May 2019 09:01:42 +0800 Subject: [PATCH 05/16] Update the mint adapter docs --- lib/tesla/adapter/mint.ex | 24 +++++++++++++++++++++++- mix.exs | 7 ++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index cc2eee82..bcc2d525 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -1,5 +1,27 @@ defmodule Tesla.Adapter.Mint do - @moduledoc false + @moduledoc """ + Adapter for [mint](https://github.com/ericmj/mint) + + 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 + """ @behaviour Tesla.Adapter import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] alias Tesla.Multipart diff --git a/mix.exs b/mix.exs index 8c438659..7eb3b7f5 100644 --- a/mix.exs +++ b/mix.exs @@ -61,8 +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"}, - {:mint, "~> 0.2.0"}, + {:castore, "~> 0.1.0", optional: true}, + {:mint, "~> 0.2.0", optional: true}, # json parsers {:jason, ">= 1.0.0", optional: true}, @@ -96,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, From e57d69793177d2cc7f5cf87410a3fbf4b31f298d Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Fri, 10 May 2019 09:15:01 +0800 Subject: [PATCH 06/16] Throwing error tuple when encountering mint error --- lib/tesla/adapter/mint.ex | 45 ++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index bcc2d525..bede4724 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -45,10 +45,12 @@ defmodule Tesla.Adapter.Mint do %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 = case env.method do - :head -> "GET" - m -> m |> Atom.to_string() |> String.upcase() - end + + method = + case env.method do + :head -> "GET" + m -> m |> Atom.to_string() |> String.upcase() + end request( method, @@ -61,7 +63,7 @@ defmodule Tesla.Adapter.Mint do 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) @@ -74,19 +76,25 @@ defmodule Tesla.Adapter.Mint do 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), + 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) - do + {:ok, conn, _req_ref} <- + HTTP.request( + conn, + method, + path || "/", + headers ++ [{"content-length", "#{length}"}], + body + ), + {:ok, _conn, res = %{status: status, headers: headers}} <- stream_response(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), + 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) do {:ok, status, headers, Map.get(res, :data)} @@ -97,10 +105,12 @@ defmodule Tesla.Adapter.Mint 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} + {:ok, body, byte_size(body)} end end @@ -123,18 +133,23 @@ defmodule Tesla.Adapter.Mint do {:done, _req_ref} -> Map.put(acc, :done, true) + + _ -> + acc end end) if Map.get(response, :done) do - response = Map.drop(response, [:done]) - {:ok, conn, response} + {:ok, conn, Map.drop(response, [:done])} else stream_response(conn, response) end - _ -> - {:error, "TODO: Error Handle"} + {:error, _conn, error, _res} -> + {:error, "Encounter Mint error #{inspect(error)}"} + + :unknown -> + {:error, "Encounter unknown error"} end end end From 0dad600698cb22c58aeec1c4c35c760286f6c863 Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Fri, 10 May 2019 13:50:50 +0800 Subject: [PATCH 07/16] Fix the unit tests for mint --- config/config.exs | 2 + lib/tesla/adapter/mint.ex | 285 +++++++++++++++++-------------- mix.exs | 13 +- test/tesla/adapter/mint_test.exs | 16 +- 4 files changed, 177 insertions(+), 139 deletions(-) diff --git a/config/config.exs b/config/config.exs index 12801b36..d4667f35 100644 --- a/config/config.exs +++ b/config/config.exs @@ -18,4 +18,6 @@ if Mix.env() == :test do sasl_error_logger: false config :tesla, MockClient, adapter: Tesla.Mock + + config :tesla, cacert: ["./deps/httparrot/priv/ssl/server-ca.crt"] end diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index bede4724..34ed8dcd 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -1,156 +1,179 @@ -defmodule Tesla.Adapter.Mint do - @moduledoc """ - Adapter for [mint](https://github.com/ericmj/mint) +if Version.compare(System.version(), "1.5.0") != :lt do + defmodule Tesla.Adapter.Mint do + @moduledoc """ + Adapter for [mint](https://github.com/ericmj/mint) - Remember to add `{:mint, "~> 0.2.0"}` and `{:castore, "~> 0.1.0"}` to dependencies - Also, you need to recompile tesla after adding `:mint` dependency: + 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 - ``` + ``` + mix deps.clean tesla + mix deps.compile tesla + ``` - ### Example usage - ``` - # set globally in config/config.exs - config :tesla, :adapter, Tesla.Adapter.Mint + ### Example usage + ``` + # set globally in config/config.exs + config :tesla, :adapter, Tesla.Adapter.Mint - # set per module - defmodule MyClient do - use Tesla + # set per module + defmodule MyClient do + use Tesla - adapter Tesla.Adapter.Mint - end - """ - @behaviour Tesla.Adapter - import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] - alias Tesla.Multipart - alias Mint.HTTP - - @doc false - def call(env, opts) do - with {:ok, status, headers, body} <- request(env, opts) do - {:ok, %{env | status: status, headers: format_headers(headers), body: body}} + adapter Tesla.Adapter.Mint + end + """ + @behaviour Tesla.Adapter + import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] + alias Tesla.Multipart + alias Mint.HTTP + + @doc false + def call(env, opts) do + with {:ok, status, headers, body} <- request(env, opts) do + {:ok, %{env | status: status, headers: format_headers(headers), body: body}} + end end - end - defp format_headers(headers) do - for {key, value} <- headers do - {String.downcase(to_string(key)), to_string(value)} + defp format_headers(headers) do + for {key, value} <- headers do + {String.downcase(to_string(key)), to_string(value)} + end 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) + 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 = - case env.method do - :head -> "GET" - m -> m |> Atom.to_string() |> String.upcase() - end + method = + case env.method do + :head -> "GET" + m -> m |> Atom.to_string() |> String.upcase() + end - request( - method, - scheme, - host, - port, - path, - env.headers, - env.body, - opts - ) - end + opts = + if opts |> get_in([:transport_opts, :cacertfile]) |> is_nil() && scheme == "https" && + !is_nil(get_default_ca()) do + transport = + opts + |> Access.get(:transport_opts, []) + |> update_in([:cacertfile], fn _ -> + get_default_ca() + end) + + update_in(opts, [:transport_opts], fn _ -> + transport + end) + else + 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 + request( + method, + scheme, + host, + port, + path, + env.headers, + env.body, + 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, %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, 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) do - {:ok, status, headers, Map.get(res, :data)} + 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 - 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) do - {:ok, status, headers, Map.get(res, :data)} + 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) do + {:ok, status, headers, Map.get(res, :data)} + end end - 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)) + 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) do + {:ok, status, headers, Map.get(res, :data)} + end + end - {:ok, item, fun} -> - stream_request(fun, body <> item) + defp get_default_ca() do + Application.get_env(:tesla, :cacert) + end - :eof -> - {:ok, body, byte_size(body)} + 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 - end - defp stream_response(conn, response \\ %{}) do - receive do - msg -> - case HTTP.stream(conn, msg) do - {:ok, conn, stream} -> - response = - Enum.reduce(stream, response, fn x, acc -> - case x do - {:status, _req_ref, code} -> - Map.put(acc, :status, code) - - {:headers, _req_ref, headers} -> - Map.put(acc, :headers, headers) - - {:data, _req_ref, data} -> - Map.put(acc, :data, Map.get(acc, :data, "") <> data) - - {:done, _req_ref} -> - Map.put(acc, :done, true) - - _ -> - acc - end - end) - - if Map.get(response, :done) do - {:ok, conn, Map.drop(response, [:done])} - else - stream_response(conn, response) - end - - {:error, _conn, error, _res} -> - {:error, "Encounter Mint error #{inspect(error)}"} - - :unknown -> - {:error, "Encounter unknown error"} - end + defp stream_response(conn, response \\ %{}) do + receive do + msg -> + case HTTP.stream(conn, msg) do + {:ok, conn, stream} -> + response = + Enum.reduce(stream, response, fn x, acc -> + case x do + {:status, _req_ref, code} -> + Map.put(acc, :status, code) + + {:headers, _req_ref, headers} -> + Map.put(acc, :headers, headers) + + {:data, _req_ref, data} -> + Map.put(acc, :data, Map.get(acc, :data, "") <> data) + + {:done, _req_ref} -> + Map.put(acc, :done, true) + + _ -> + acc + end + end) + + if Map.get(response, :done) do + {:ok, conn, Map.drop(response, [:done])} + else + stream_response(conn, response) + end + + {:error, _conn, error, _res} -> + {:error, "Encounter Mint error #{inspect(error)}"} + + :unknown -> + {:error, "Encounter unknown error"} + end + end end end end diff --git a/mix.exs b/mix.exs index 7eb3b7f5..ee44bf91 100644 --- a/mix.exs +++ b/mix.exs @@ -80,7 +80,18 @@ defmodule Tesla.Mixfile do {:mix_test_watch, "~> 0.5", only: :dev}, {:dialyxir, "~> 1.0.0-rc.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5.6", only: :docs} - ] + ] ++ mint_version_check() + end + + defp mint_version_check() do + if Version.compare(System.version(), "1.5.0") != :lt do + [ + {:castore, "~> 0.1.0", optional: true}, + {:mint, "~> 0.2.0", optional: true} + ] + else + [] + end end defp docs do diff --git a/test/tesla/adapter/mint_test.exs b/test/tesla/adapter/mint_test.exs index 5b8f63ba..3bb22f5f 100644 --- a/test/tesla/adapter/mint_test.exs +++ b/test/tesla/adapter/mint_test.exs @@ -1,9 +1,11 @@ -defmodule Tesla.Adapter.MintTest do - use ExUnit.Case +if Version.compare(System.version(), "1.5.0") != :lt do + 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 + use Tesla.AdapterCase, adapter: Tesla.Adapter.Mint + use Tesla.AdapterCase.Basic + use Tesla.AdapterCase.Multipart + use Tesla.AdapterCase.StreamRequestBody + use Tesla.AdapterCase.SSL + end end From c28cccc32638a465d218be28d5efcc3a0df186ba Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Sat, 11 May 2019 21:11:31 +0800 Subject: [PATCH 08/16] Update README about the cacert and minimum elixir version for mint --- lib/tesla/adapter/mint.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 34ed8dcd..962ae507 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -3,6 +3,8 @@ if Version.compare(System.version(), "1.5.0") != :lt 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: @@ -22,6 +24,9 @@ if Version.compare(System.version(), "1.5.0") != :lt do adapter Tesla.Adapter.Mint end + + # set custom cacert + config :tesla, :cacert, ["path_to_cacert"] """ @behaviour Tesla.Adapter import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] From ae07e699c2a80378d87b6207225f7ed19751e33d Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Sun, 2 Jun 2019 18:29:37 +0800 Subject: [PATCH 09/16] Bump the minimum elixir version as 1.5.0 --- .travis.yml | 2 + lib/tesla/adapter/mint.ex | 304 +++++++++++++++---------------- mix.exs | 15 +- test/tesla/adapter/mint_test.exs | 16 +- 4 files changed, 162 insertions(+), 175 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd6e568a..89011185 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ env: - MIX_ENV=test matrix: include: + - elixir: 1.5.3 + otp_release: 18.3 - elixir: 1.5.3 otp_release: 19.3 - elixir: 1.5.3 diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 962ae507..c9d599c6 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -1,184 +1,182 @@ -if Version.compare(System.version(), "1.5.0") != :lt do - defmodule Tesla.Adapter.Mint do - @moduledoc """ - Adapter for [mint](https://github.com/ericmj/mint) +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 + 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: + 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 - ``` + ``` + mix deps.clean tesla + mix deps.compile tesla + ``` - ### Example usage - ``` - # set globally in config/config.exs - config :tesla, :adapter, Tesla.Adapter.Mint + ### Example usage + ``` + # set globally in config/config.exs + config :tesla, :adapter, Tesla.Adapter.Mint - # set per module - defmodule MyClient do - use Tesla + # set per module + defmodule MyClient do + use Tesla - adapter Tesla.Adapter.Mint - end + adapter Tesla.Adapter.Mint + end - # set custom cacert - config :tesla, :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 - - @doc false - def call(env, opts) do - with {:ok, status, headers, body} <- request(env, opts) do - {:ok, %{env | status: status, headers: format_headers(headers), body: body}} - end + # set custom cacert + config :tesla, :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 + + @doc false + def call(env, opts) do + with {:ok, status, headers, body} <- request(env, opts) do + {:ok, %{env | status: status, headers: format_headers(headers), body: body}} end + end - defp format_headers(headers) do - for {key, value} <- headers do - {String.downcase(to_string(key)), to_string(value)} - end + defp format_headers(headers) do + for {key, value} <- headers do + {String.downcase(to_string(key)), to_string(value)} 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) + 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 = - case env.method do - :head -> "GET" - m -> m |> Atom.to_string() |> String.upcase() - end + method = + case env.method do + :head -> "GET" + m -> m |> Atom.to_string() |> String.upcase() + end - opts = - if opts |> get_in([:transport_opts, :cacertfile]) |> is_nil() && scheme == "https" && - !is_nil(get_default_ca()) do - transport = - opts - |> Access.get(:transport_opts, []) - |> update_in([:cacertfile], fn _ -> - get_default_ca() - end) - - update_in(opts, [:transport_opts], fn _ -> - transport - end) - else + opts = + if opts |> get_in([:transport_opts, :cacertfile]) |> is_nil() && scheme == "https" && + !is_nil(get_default_ca()) do + transport = opts - end + |> Access.get(:transport_opts, []) + |> update_in([:cacertfile], fn _ -> + get_default_ca() + end) - request( - method, - scheme, - host, - port, - path, - env.headers, - env.body, + update_in(opts, [:transport_opts], fn _ -> + transport + end) + else opts - ) - end + 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 + request( + method, + scheme, + host, + port, + path, + env.headers, + env.body, + 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, %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, 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) do - {:ok, status, headers, Map.get(res, :data)} - end - 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) 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) do - {:ok, status, headers, Map.get(res, :data)} - 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) do + {:ok, status, headers, Map.get(res, :data)} end + end - defp get_default_ca() do - Application.get_env(:tesla, :cacert) + 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) do + {:ok, status, headers, Map.get(res, :data)} end + end + + defp get_default_ca() do + Application.get_env(:tesla, :cacert) + 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)) + 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) + {:ok, item, fun} -> + stream_request(fun, body <> item) - :eof -> - {:ok, body, byte_size(body)} - end + :eof -> + {:ok, body, byte_size(body)} end + end - defp stream_response(conn, response \\ %{}) do - receive do - msg -> - case HTTP.stream(conn, msg) do - {:ok, conn, stream} -> - response = - Enum.reduce(stream, response, fn x, acc -> - case x do - {:status, _req_ref, code} -> - Map.put(acc, :status, code) - - {:headers, _req_ref, headers} -> - Map.put(acc, :headers, headers) - - {:data, _req_ref, data} -> - Map.put(acc, :data, Map.get(acc, :data, "") <> data) - - {:done, _req_ref} -> - Map.put(acc, :done, true) - - _ -> - acc - end - end) - - if Map.get(response, :done) do - {:ok, conn, Map.drop(response, [:done])} - else - stream_response(conn, response) - end - - {:error, _conn, error, _res} -> - {:error, "Encounter Mint error #{inspect(error)}"} - - :unknown -> - {:error, "Encounter unknown error"} - end - end + defp stream_response(conn, response \\ %{}) do + receive do + msg -> + case HTTP.stream(conn, msg) do + {:ok, conn, stream} -> + response = + Enum.reduce(stream, response, fn x, acc -> + case x do + {:status, _req_ref, code} -> + Map.put(acc, :status, code) + + {:headers, _req_ref, headers} -> + Map.put(acc, :headers, headers) + + {:data, _req_ref, data} -> + Map.put(acc, :data, Map.get(acc, :data, "") <> data) + + {:done, _req_ref} -> + Map.put(acc, :done, true) + + _ -> + acc + end + end) + + if Map.get(response, :done) do + {:ok, conn, Map.drop(response, [:done])} + else + stream_response(conn, response) + end + + {:error, _conn, error, _res} -> + {:error, "Encounter Mint error #{inspect(error)}"} + + :unknown -> + {:error, "Encounter unknown error"} + end end end end diff --git a/mix.exs b/mix.exs index ee44bf91..9203b6e3 100644 --- a/mix.exs +++ b/mix.exs @@ -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")), @@ -80,18 +80,7 @@ defmodule Tesla.Mixfile do {:mix_test_watch, "~> 0.5", only: :dev}, {:dialyxir, "~> 1.0.0-rc.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5.6", only: :docs} - ] ++ mint_version_check() - end - - defp mint_version_check() do - if Version.compare(System.version(), "1.5.0") != :lt do - [ - {:castore, "~> 0.1.0", optional: true}, - {:mint, "~> 0.2.0", optional: true} - ] - else - [] - end + ] end defp docs do diff --git a/test/tesla/adapter/mint_test.exs b/test/tesla/adapter/mint_test.exs index 3bb22f5f..5b8f63ba 100644 --- a/test/tesla/adapter/mint_test.exs +++ b/test/tesla/adapter/mint_test.exs @@ -1,11 +1,9 @@ -if Version.compare(System.version(), "1.5.0") != :lt do - defmodule Tesla.Adapter.MintTest do - use ExUnit.Case +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 - end + use Tesla.AdapterCase, adapter: Tesla.Adapter.Mint + use Tesla.AdapterCase.Basic + use Tesla.AdapterCase.Multipart + use Tesla.AdapterCase.StreamRequestBody + use Tesla.AdapterCase.SSL end From b487fde6b23b476470c4b1d317cf8ac70d00e981 Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Sun, 2 Jun 2019 18:37:37 +0800 Subject: [PATCH 10/16] Make the cacert as the property for Mint --- config/config.exs | 2 +- lib/tesla/adapter/mint.ex | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/config.exs b/config/config.exs index d4667f35..0548fa05 100644 --- a/config/config.exs +++ b/config/config.exs @@ -19,5 +19,5 @@ if Mix.env() == :test do config :tesla, MockClient, adapter: Tesla.Mock - config :tesla, cacert: ["./deps/httparrot/priv/ssl/server-ca.crt"] + config :tesla, Mint, cacert: ["./deps/httparrot/priv/ssl/server-ca.crt"] end diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index c9d599c6..57051867 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -25,7 +25,7 @@ defmodule Tesla.Adapter.Mint do end # set custom cacert - config :tesla, :cacert, ["path_to_cacert"] + config :tesla, Mint, cacert: ["path_to_cacert"] """ @behaviour Tesla.Adapter import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] @@ -124,7 +124,8 @@ defmodule Tesla.Adapter.Mint do end defp get_default_ca() do - Application.get_env(:tesla, :cacert) + env = Application.get_env(:tesla, Mint) + Keyword.get(env, :cacert) end defp stream_request(fun, body \\ "") do From 2d57f4e8022b2768e4f28e556a78bef43f814793 Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Sun, 2 Jun 2019 18:39:59 +0800 Subject: [PATCH 11/16] Remove unnecessary check for http method in mint adapter --- lib/tesla/adapter/mint.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 57051867..92198805 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -51,11 +51,7 @@ defmodule Tesla.Adapter.Mint do query = (query || "") |> URI.decode_query() |> Map.to_list() path = Tesla.build_url(path, env.query ++ query) - method = - case env.method do - :head -> "GET" - m -> m |> Atom.to_string() |> String.upcase() - end + method = env.method |> Atom.to_string() |> String.upcase() opts = if opts |> get_in([:transport_opts, :cacertfile]) |> is_nil() && scheme == "https" && From 046822bb9cb7a967717dc6241ffb80859410bfba Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Tue, 4 Jun 2019 22:24:21 +0800 Subject: [PATCH 12/16] Improve the usage in mint library --- config/config.exs | 3 +- lib/tesla/adapter/mint.ex | 53 ++++++++++++++++++-------------- test/tesla/adapter/mint_test.exs | 9 ++++++ 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/config/config.exs b/config/config.exs index 0548fa05..2c43344c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -19,5 +19,6 @@ if Mix.env() == :test do config :tesla, MockClient, adapter: Tesla.Mock - config :tesla, Mint, cacert: ["./deps/httparrot/priv/ssl/server-ca.crt"] + config :tesla, Tesla.Adapter.Mint, + cacert: ["./deps/httparrot/priv/ssl/server-ca.crt"] end diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 92198805..da8c4dd7 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -32,16 +32,14 @@ defmodule Tesla.Adapter.Mint do alias Tesla.Multipart alias Mint.HTTP + @default adapter: [timeout: 2_000] + @doc false def call(env, opts) do - with {:ok, status, headers, body} <- request(env, opts) do - {:ok, %{env | status: status, headers: format_headers(headers), body: body}} - end - end + opts = Tesla.Adapter.opts(@default, env, opts) - defp format_headers(headers) do - for {key, value} <- headers do - {String.downcase(to_string(key)), to_string(value)} + with {:ok, status, headers, body} <- request(env, opts) do + {:ok, %{env | status: status, headers: headers, body: body}} end end @@ -59,13 +57,9 @@ defmodule Tesla.Adapter.Mint do transport = opts |> Access.get(:transport_opts, []) - |> update_in([:cacertfile], fn _ -> - get_default_ca() - end) + |> Keyword.put(:cacertfile, get_default_ca()) - update_in(opts, [:transport_opts], fn _ -> - transport - end) + Keyword.put(opts, :transport_opts, transport) else opts end @@ -106,21 +100,23 @@ defmodule Tesla.Adapter.Mint do headers ++ [{"content-length", "#{length}"}], body ), - {:ok, _conn, res = %{status: status, headers: headers}} <- stream_response(conn) do + {: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) do + {: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_default_ca() do - env = Application.get_env(:tesla, Mint) + env = Application.get_env(:tesla, Tesla.Adapter.Mint) Keyword.get(env, :cacert) end @@ -137,7 +133,7 @@ defmodule Tesla.Adapter.Mint do end end - defp stream_response(conn, response \\ %{}) do + defp stream_response(conn, opts, response \\ %{}) do receive do msg -> case HTTP.stream(conn, msg) do @@ -149,7 +145,7 @@ defmodule Tesla.Adapter.Mint do Map.put(acc, :status, code) {:headers, _req_ref, headers} -> - Map.put(acc, :headers, headers) + Map.put(acc, :headers, Map.get(acc, :headers, []) ++ headers) {:data, _req_ref, data} -> Map.put(acc, :data, Map.get(acc, :data, "") <> data) @@ -157,15 +153,23 @@ defmodule Tesla.Adapter.Mint do {:done, _req_ref} -> Map.put(acc, :done, true) + {:error, _req_ref, reason} -> + Map.put(acc, :error, reason) + _ -> acc end end) - if Map.get(response, :done) do - {:ok, conn, Map.drop(response, [:done])} - else - stream_response(conn, response) + 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, response, opts) end {:error, _conn, error, _res} -> @@ -174,6 +178,9 @@ defmodule Tesla.Adapter.Mint do :unknown -> {:error, "Encounter unknown error"} end + after + opts |> Keyword.get(:adapter) |> Keyword.get(:timeout) -> + {:error, "Response timeout"} end end end diff --git a/test/tesla/adapter/mint_test.exs b/test/tesla/adapter/mint_test.exs index 5b8f63ba..6af16f81 100644 --- a/test/tesla/adapter/mint_test.exs +++ b/test/tesla/adapter/mint_test.exs @@ -6,4 +6,13 @@ defmodule Tesla.Adapter.MintTest do 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 From 9e7b7d2e3814d79223b7cd6e91207b43ab93a74c Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Tue, 4 Jun 2019 22:45:21 +0800 Subject: [PATCH 13/16] Refactor the default cacert as global cacert --- lib/tesla/adapter/mint.ex | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index da8c4dd7..079cc04c 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -24,8 +24,8 @@ defmodule Tesla.Adapter.Mint do adapter Tesla.Adapter.Mint end - # set custom cacert - config :tesla, Mint, cacert: ["path_to_cacert"] + # 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] @@ -51,15 +51,19 @@ defmodule Tesla.Adapter.Mint do method = env.method |> Atom.to_string() |> String.upcase() + # Set the global cacert file opts = - if opts |> get_in([:transport_opts, :cacertfile]) |> is_nil() && scheme == "https" && - !is_nil(get_default_ca()) do - transport = - opts - |> Access.get(:transport_opts, []) - |> Keyword.put(:cacertfile, get_default_ca()) - - Keyword.put(opts, :transport_opts, transport) + 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 @@ -115,7 +119,7 @@ defmodule Tesla.Adapter.Mint do end end - defp get_default_ca() do + defp get_global_default_ca() do env = Application.get_env(:tesla, Tesla.Adapter.Mint) Keyword.get(env, :cacert) end From eef0adc6c6cca833ee5cb342f3459460f6360025 Mon Sep 17 00:00:00 2001 From: "Ryan, Siu Long Wa" Date: Wed, 19 Jun 2019 21:48:41 +0800 Subject: [PATCH 14/16] Fix the incorrect order of stream_response Co-Authored-By: Alexander Strizhakov --- lib/tesla/adapter/mint.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 079cc04c..506ac49e 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -173,7 +173,7 @@ defmodule Tesla.Adapter.Mint do {:ok, conn, Map.drop(response, [:done])} true -> - stream_response(conn, response, opts) + stream_response(conn, opts, response) end {:error, _conn, error, _res} -> From 8abab5d41d858d8107f3330da3c6dea318d088d7 Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Tue, 25 Jun 2019 09:34:36 +0800 Subject: [PATCH 15/16] Refactor the codes for mint adapter --- lib/tesla/adapter/mint.ex | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/tesla/adapter/mint.ex b/lib/tesla/adapter/mint.ex index 506ac49e..83cc6de3 100644 --- a/lib/tesla/adapter/mint.ex +++ b/lib/tesla/adapter/mint.ex @@ -120,8 +120,10 @@ defmodule Tesla.Adapter.Mint do end defp get_global_default_ca() do - env = Application.get_env(:tesla, Tesla.Adapter.Mint) - Keyword.get(env, :cacert) + case Application.get_env(:tesla, Tesla.Adapter.Mint) do + nil -> nil + env -> Keyword.get(env, :cacert) + end end defp stream_request(fun, body \\ "") do @@ -143,26 +145,24 @@ defmodule Tesla.Adapter.Mint do case HTTP.stream(conn, msg) do {:ok, conn, stream} -> response = - Enum.reduce(stream, response, fn x, acc -> - case x do - {:status, _req_ref, code} -> - Map.put(acc, :status, code) + Enum.reduce(stream, response, fn + {:status, _req_ref, code}, acc -> + Map.put(acc, :status, code) - {:headers, _req_ref, headers} -> - Map.put(acc, :headers, Map.get(acc, :headers, []) ++ headers) + {:headers, _req_ref, headers}, acc -> + Map.put(acc, :headers, Map.get(acc, :headers, []) ++ headers) - {:data, _req_ref, data} -> - Map.put(acc, :data, Map.get(acc, :data, "") <> data) + {:data, _req_ref, data}, acc -> + Map.put(acc, :data, Map.get(acc, :data, "") <> data) - {:done, _req_ref} -> - Map.put(acc, :done, true) + {:done, _req_ref}, acc -> + Map.put(acc, :done, true) - {:error, _req_ref, reason} -> - Map.put(acc, :error, reason) + {:error, _req_ref, reason}, acc -> + Map.put(acc, :error, reason) - _ -> - acc - end + _, acc -> + acc end) cond do From b1e5c11afbe431adcbd9a405af9c52182529a8af Mon Sep 17 00:00:00 2001 From: Ryan SIU Date: Wed, 26 Jun 2019 11:17:40 +0800 Subject: [PATCH 16/16] Remove OTP 18.3 due to the unsupport from travis --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 89011185..bd6e568a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ env: - MIX_ENV=test matrix: include: - - elixir: 1.5.3 - otp_release: 18.3 - elixir: 1.5.3 otp_release: 19.3 - elixir: 1.5.3