diff --git a/lib/req.ex b/lib/req.ex index ba36548..b970095 100644 --- a/lib/req.ex +++ b/lib/req.ex @@ -135,6 +135,8 @@ defmodule Req do @req Req.Request.new() |> Req.Steps.attach() + @default_finch_options Req.Finch.pool_options(%{}) + @doc """ Returns a new request struct with built-in steps. @@ -361,7 +363,8 @@ defmodule Req do * `:timeout` - socket connect timeout in milliseconds, defaults to `30_000`. - * `:protocols` - the HTTP protocols to use, defaults to `#{inspect(Req.Application.__default_protocols__())}`. + * `:protocols` - the HTTP protocols to use, defaults to + `#{inspect(Keyword.fetch!(@default_finch_options, :protocols))}`. * `:hostname` - Mint explicit hostname. diff --git a/lib/req/application.ex b/lib/req/application.ex index e742faf..00da0f8 100644 --- a/lib/req/application.ex +++ b/lib/req/application.ex @@ -9,9 +9,7 @@ defmodule Req.Application do {Finch, name: Req.Finch, pools: %{ - default: [ - protocols: __default_protocols__() - ] + default: Req.Finch.pool_options(%{}) }}, {DynamicSupervisor, strategy: :one_for_one, name: Req.FinchSupervisor}, {Req.Test.Ownership, name: Req.Test.Ownership} @@ -19,8 +17,4 @@ defmodule Req.Application do Supervisor.start_link(children, strategy: :one_for_one) end - - def __default_protocols__ do - [:http1] - end end diff --git a/lib/req/finch.ex b/lib/req/finch.ex new file mode 100644 index 0000000..c7fc07e --- /dev/null +++ b/lib/req/finch.ex @@ -0,0 +1,397 @@ +defmodule Req.Finch do + @moduledoc false + + @doc """ + Runs the request using `Finch`. + """ + def run(request) do + # URI.parse removes `[` and `]` so we can't check for these. The host + # should not have `:` so it should be safe to check for it. + request = + if !request.options[:inet6] and + (request.url.host || "") =~ ":" do + request = put_in(request.options[:inet6], true) + # ...and have to put them back for host header. + Req.Request.put_new_header(request, "host", "[#{request.url.host}]") + else + request + end + + finch_name = finch_name(request) + + request_headers = + if unquote(Req.MixProject.legacy_headers_as_lists?()) do + request.headers + else + for {name, values} <- request.headers, + value <- values do + {name, value} + end + end + + body = + case request.body do + iodata when is_binary(iodata) or is_list(iodata) -> + iodata + + nil -> + nil + + enumerable -> + {:stream, enumerable} + end + + finch_request = + Finch.build(request.method, request.url, request_headers, body) + |> Map.replace!(:unix_socket, request.options[:unix_socket]) + |> add_private_options(request.options[:finch_private]) + + finch_options = + request.options |> Map.take([:receive_timeout, :pool_timeout]) |> Enum.to_list() + + run(request, finch_request, finch_name, finch_options) + end + + defp run(req, finch_req, finch_name, finch_options) do + case req.options[:finch_request] do + fun when is_function(fun, 4) -> + fun.(req, finch_req, finch_name, finch_options) + + deprecated_fun when is_function(deprecated_fun, 1) -> + IO.warn( + "passing a :finch_request function accepting a single argument is deprecated. " <> + "See Req.Steps.run_finch/1 for more information." + ) + + {req, run_finch_request(deprecated_fun.(finch_req), finch_name, finch_options)} + + nil -> + case req.into do + nil -> + {req, run_finch_request(finch_req, finch_name, finch_options)} + + fun when is_function(fun, 2) -> + finch_stream_into_fun(req, finch_req, finch_name, finch_options, fun) + + :legacy_self -> + finch_stream_into_legacy_self(req, finch_req, finch_name, finch_options) + + :self -> + finch_stream_into_self(req, finch_req, finch_name, finch_options) + + collectable -> + finch_stream_into_collectable(req, finch_req, finch_name, finch_options, collectable) + end + end + end + + defp finch_stream_into_fun(req, finch_req, finch_name, finch_options, fun) do + resp = Req.Response.new() + + fun = fn + {:status, status}, {req, resp} -> + {:cont, {req, %{resp | status: status}}} + + {:headers, fields}, {req, resp} -> + fields = finch_fields_to_map(fields) + resp = update_in(resp.headers, &Map.merge(&1, fields)) + {:cont, {req, resp}} + + {:data, data}, acc -> + fun.({:data, data}, acc) + + {:trailers, fields}, {req, resp} -> + fields = finch_fields_to_map(fields) + resp = update_in(resp.trailers, &Map.merge(&1, fields)) + {:cont, {req, resp}} + end + + case Finch.stream_while(finch_req, finch_name, {req, resp}, fun, finch_options) do + {:ok, acc} -> + acc + + {:error, %Mint.TransportError{reason: reason}} -> + {req, %Req.TransportError{reason: reason}} + + {:error, %Mint.HTTPError{module: Mint.HTTP1, reason: reason}} -> + {req, %Req.HTTPError{protocol: :http1, reason: reason}} + + {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: reason}} -> + {req, %Req.HTTPError{protocol: :http2, reason: reason}} + + {:error, %Finch.Error{reason: reason}} -> + {req, %Req.HTTPError{protocol: :http2, reason: reason}} + + {:error, exception} -> + {req, exception} + end + end + + defp finch_stream_into_collectable(req, finch_req, finch_name, finch_options, collectable) do + {acc, collector} = Collectable.into(collectable) + resp = Req.Response.new() + + fun = fn + {:status, status}, {acc, req, resp} -> + {acc, req, %{resp | status: status}} + + {:headers, fields}, {acc, req, resp} -> + fields = finch_fields_to_map(fields) + resp = update_in(resp.headers, &Map.merge(&1, fields)) + {acc, req, resp} + + {:data, data}, {acc, req, resp} -> + acc = collector.(acc, {:cont, data}) + {acc, req, resp} + + {:trailers, fields}, {acc, req, resp} -> + fields = finch_fields_to_map(fields) + resp = update_in(resp.trailers, &Map.merge(&1, fields)) + {acc, req, resp} + end + + case Finch.stream(finch_req, finch_name, {acc, req, resp}, fun, finch_options) do + {:ok, {acc, req, resp}} -> + acc = collector.(acc, :done) + {req, %{resp | body: acc}} + + {:error, %Mint.TransportError{reason: reason}} -> + {req, %Req.TransportError{reason: reason}} + + {:error, %Mint.HTTPError{module: Mint.HTTP1, reason: reason}} -> + {req, %Req.HTTPError{protocol: :http1, reason: reason}} + + {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: reason}} -> + {req, %Req.HTTPError{protocol: :http2, reason: reason}} + + {:error, %Finch.Error{reason: reason}} -> + {req, %Req.HTTPError{protocol: :http2, reason: reason}} + + {:error, exception} -> + {req, exception} + end + end + + defp finch_stream_into_legacy_self(req, finch_req, finch_name, finch_options) do + ref = Finch.async_request(finch_req, finch_name, finch_options) + + {:status, status} = + receive do + {^ref, message} -> + message + end + + headers = + receive do + {^ref, message} -> + {:headers, headers} = message + + Enum.reduce(headers, %{}, fn {name, value}, acc -> + Map.update(acc, name, [value], &(&1 ++ [value])) + end) + end + + async = %Req.Response.Async{ + pid: self(), + ref: ref, + stream_fun: &finch_parse_message/2, + cancel_fun: &finch_cancel/1 + } + + req = put_in(req.async, async) + resp = Req.Response.new(status: status, headers: headers) + {req, resp} + end + + defp finch_stream_into_self(req, finch_req, finch_name, finch_options) do + ref = Finch.async_request(finch_req, finch_name, finch_options) + + {:status, status} = + receive do + {^ref, message} -> + message + end + + headers = + receive do + {^ref, message} -> + # TODO: handle trailers + {:headers, headers} = message + + Enum.reduce(headers, %{}, fn {name, value}, acc -> + Map.update(acc, name, [value], &(&1 ++ [value])) + end) + end + + async = %Req.Response.Async{ + pid: self(), + ref: ref, + stream_fun: &finch_parse_message/2, + cancel_fun: &finch_cancel/1 + } + + resp = Req.Response.new(status: status, headers: headers) + resp = put_in(resp.body, async) + {req, resp} + end + + defp run_finch_request(finch_request, finch_name, finch_options) do + case Finch.request(finch_request, finch_name, finch_options) do + {:ok, response} -> + Req.Response.new(response) + + {:error, %Mint.TransportError{reason: reason}} -> + %Req.TransportError{reason: reason} + + {:error, %Mint.HTTPError{module: Mint.HTTP1, reason: reason}} -> + %Req.HTTPError{protocol: :http1, reason: reason} + + {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: reason}} -> + %Req.HTTPError{protocol: :http2, reason: reason} + + {:error, %Finch.Error{reason: reason}} -> + %Req.HTTPError{protocol: :http2, reason: reason} + + {:error, exception} -> + exception + end + end + + defp add_private_options(finch_request, nil) do + finch_request + end + + defp add_private_options(finch_request, private_options) + when is_list(private_options) or is_map(private_options) do + Enum.reduce(private_options, finch_request, fn {k, v}, acc_finch_req -> + Finch.Request.put_private(acc_finch_req, k, v) + end) + end + + defp finch_fields_to_map(fields) do + Enum.reduce(fields, %{}, fn {name, value}, acc -> + Map.update(acc, name, [value], &(&1 ++ [value])) + end) + end + + defp finch_parse_message(ref, {ref, {:data, data}}) do + {:ok, [data: data]} + end + + defp finch_parse_message(ref, {ref, :done}) do + {:ok, [:done]} + end + + defp finch_parse_message(ref, {ref, {:trailers, trailers}}) do + {:ok, [trailers: trailers]} + end + + defp finch_parse_message(ref, {ref, {:error, reason}}) do + {:error, reason} + end + + defp finch_cancel(ref) do + Finch.cancel_async_request(ref) + end + + defp finch_name(request) do + custom_options? = + Map.has_key?(request.options, :connect_options) or Map.has_key?(request.options, :inet6) + + cond do + request.options[:finch] && custom_options? -> + raise ArgumentError, "cannot set both :finch and :connect_options" + + custom_options? -> + pool_options = pool_options(request.options) + + name = + pool_options + |> :erlang.term_to_binary() + |> :erlang.md5() + |> Base.url_encode64(padding: false) + + name = Module.concat(Req.FinchSupervisor, "Pool_#{name}") + + case DynamicSupervisor.start_child( + Req.FinchSupervisor, + {Finch, name: name, pools: %{default: pool_options}} + ) do + {:ok, _} -> + name + + {:error, {:already_started, _}} -> + name + end + + true -> + Req.Finch + end + end + + @doc """ + Returns Finch pool options for the given Req `options`. + """ + def pool_options(options) when is_map(options) do + connect_options = options[:connect_options] || [] + inet6_options = options |> Map.take([:inet6]) |> Enum.to_list() + + Req.Request.validate_options( + connect_options, + MapSet.new([ + :timeout, + :protocols, + :transport_opts, + :proxy_headers, + :proxy, + :client_settings, + :hostname, + + # TODO: Remove on Req v1.0 + :protocol + ]) + ) + + transport_opts = + Keyword.merge( + Keyword.take(connect_options, [:timeout]) ++ inet6_options, + Keyword.get(connect_options, :transport_opts, []) + ) + + conn_opts = + Keyword.take(connect_options, [:hostname, :proxy, :proxy_headers, :client_settings]) ++ + if transport_opts != [] do + [transport_opts: transport_opts] + else + [] + end + + protocols = + cond do + protocols = connect_options[:protocols] -> + protocols + + protocol = connect_options[:protocol] -> + IO.warn([ + "setting `connect_options: [protocol: protocol]` is deprecated, ", + "use `connect_options: [protocols: protocols]` instead" + ]) + + [protocol] + + true -> + [:http1] + end + + [protocols: protocols] ++ + if conn_opts != [] do + [conn_opts: conn_opts] + else + [] + end + end + + def pool_options(options) when is_list(options) do + pool_options(Req.new(options).options) + end +end diff --git a/lib/req/steps.ex b/lib/req/steps.ex index 0243f6c..a458183 100644 --- a/lib/req/steps.ex +++ b/lib/req/steps.ex @@ -630,6 +630,8 @@ defmodule Req.Steps do end end + @default_finch_options Req.Finch.pool_options(%{}) + @doc """ Runs the request using `Finch`. @@ -669,7 +671,8 @@ defmodule Req.Steps do * `:timeout` - socket connect timeout in milliseconds, defaults to `30_000`. - * `:protocols` - the HTTP protocols to use, defaults to `#{inspect(Req.Application.__default_protocols__())}`. + * `:protocols` - the HTTP protocols to use, defaults to + `#{inspect(Keyword.fetch!(@default_finch_options, :protocols))}`. * `:hostname` - Mint explicit hostname, see `Mint.HTTP.connect/4` for more information. @@ -743,384 +746,7 @@ defmodule Req.Steps do """ @doc step: :request def run_finch(request) do - # URI.parse removes `[` and `]` so we can't check for these. The host - # should not have `:` so it should be safe to check for it. - request = - if !request.options[:inet6] and - (request.url.host || "") =~ ":" do - request = put_in(request.options[:inet6], true) - # ...and have to put them back for host header. - Req.Request.put_new_header(request, "host", "[#{request.url.host}]") - else - request - end - - finch_name = finch_name(request) - - request_headers = - if unquote(Req.MixProject.legacy_headers_as_lists?()) do - request.headers - else - for {name, values} <- request.headers, - value <- values do - {name, value} - end - end - - body = - case request.body do - iodata when is_binary(iodata) or is_list(iodata) -> - iodata - - nil -> - nil - - enumerable -> - {:stream, enumerable} - end - - finch_request = - Finch.build(request.method, request.url, request_headers, body) - |> Map.replace!(:unix_socket, request.options[:unix_socket]) - |> add_private_options(request.options[:finch_private]) - - finch_options = - request.options |> Map.take([:receive_timeout, :pool_timeout]) |> Enum.to_list() - - run_finch(request, finch_request, finch_name, finch_options) - end - - defp run_finch(req, finch_req, finch_name, finch_options) do - case req.options[:finch_request] do - fun when is_function(fun, 4) -> - fun.(req, finch_req, finch_name, finch_options) - - deprecated_fun when is_function(deprecated_fun, 1) -> - IO.warn( - "passing a :finch_request function accepting a single argument is deprecated. " <> - "See Req.Steps.run_finch/1 for more information." - ) - - {req, run_finch_request(deprecated_fun.(finch_req), finch_name, finch_options)} - - nil -> - case req.into do - nil -> - {req, run_finch_request(finch_req, finch_name, finch_options)} - - fun when is_function(fun, 2) -> - finch_stream_into_fun(req, finch_req, finch_name, finch_options, fun) - - :legacy_self -> - finch_stream_into_legacy_self(req, finch_req, finch_name, finch_options) - - :self -> - finch_stream_into_self(req, finch_req, finch_name, finch_options) - - collectable -> - finch_stream_into_collectable(req, finch_req, finch_name, finch_options, collectable) - end - end - end - - defp finch_stream_into_fun(req, finch_req, finch_name, finch_options, fun) do - resp = Req.Response.new() - - fun = fn - {:status, status}, {req, resp} -> - {:cont, {req, %{resp | status: status}}} - - {:headers, fields}, {req, resp} -> - fields = finch_fields_to_map(fields) - resp = update_in(resp.headers, &Map.merge(&1, fields)) - {:cont, {req, resp}} - - {:data, data}, acc -> - fun.({:data, data}, acc) - - {:trailers, fields}, {req, resp} -> - fields = finch_fields_to_map(fields) - resp = update_in(resp.trailers, &Map.merge(&1, fields)) - {:cont, {req, resp}} - end - - case Finch.stream_while(finch_req, finch_name, {req, resp}, fun, finch_options) do - {:ok, acc} -> - acc - - {:error, %Mint.TransportError{reason: reason}} -> - {req, %Req.TransportError{reason: reason}} - - {:error, %Mint.HTTPError{module: Mint.HTTP1, reason: reason}} -> - {req, %Req.HTTPError{protocol: :http1, reason: reason}} - - {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: reason}} -> - {req, %Req.HTTPError{protocol: :http2, reason: reason}} - - {:error, %Finch.Error{reason: reason}} -> - {req, %Req.HTTPError{protocol: :http2, reason: reason}} - - {:error, exception} -> - {req, exception} - end - end - - defp finch_stream_into_collectable(req, finch_req, finch_name, finch_options, collectable) do - {acc, collector} = Collectable.into(collectable) - resp = Req.Response.new() - - fun = fn - {:status, status}, {acc, req, resp} -> - {acc, req, %{resp | status: status}} - - {:headers, fields}, {acc, req, resp} -> - fields = finch_fields_to_map(fields) - resp = update_in(resp.headers, &Map.merge(&1, fields)) - {acc, req, resp} - - {:data, data}, {acc, req, resp} -> - acc = collector.(acc, {:cont, data}) - {acc, req, resp} - - {:trailers, fields}, {acc, req, resp} -> - fields = finch_fields_to_map(fields) - resp = update_in(resp.trailers, &Map.merge(&1, fields)) - {acc, req, resp} - end - - case Finch.stream(finch_req, finch_name, {acc, req, resp}, fun, finch_options) do - {:ok, {acc, req, resp}} -> - acc = collector.(acc, :done) - {req, %{resp | body: acc}} - - {:error, %Mint.TransportError{reason: reason}} -> - {req, %Req.TransportError{reason: reason}} - - {:error, %Mint.HTTPError{module: Mint.HTTP1, reason: reason}} -> - {req, %Req.HTTPError{protocol: :http1, reason: reason}} - - {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: reason}} -> - {req, %Req.HTTPError{protocol: :http2, reason: reason}} - - {:error, %Finch.Error{reason: reason}} -> - {req, %Req.HTTPError{protocol: :http2, reason: reason}} - - {:error, exception} -> - {req, exception} - end - end - - defp finch_stream_into_legacy_self(req, finch_req, finch_name, finch_options) do - ref = Finch.async_request(finch_req, finch_name, finch_options) - - {:status, status} = - receive do - {^ref, message} -> - message - end - - headers = - receive do - {^ref, message} -> - {:headers, headers} = message - - Enum.reduce(headers, %{}, fn {name, value}, acc -> - Map.update(acc, name, [value], &(&1 ++ [value])) - end) - end - - async = %Req.Response.Async{ - pid: self(), - ref: ref, - stream_fun: &finch_parse_message/2, - cancel_fun: &finch_cancel/1 - } - - req = put_in(req.async, async) - resp = Req.Response.new(status: status, headers: headers) - {req, resp} - end - - defp finch_stream_into_self(req, finch_req, finch_name, finch_options) do - ref = Finch.async_request(finch_req, finch_name, finch_options) - - {:status, status} = - receive do - {^ref, message} -> - message - end - - headers = - receive do - {^ref, message} -> - # TODO: handle trailers - {:headers, headers} = message - - Enum.reduce(headers, %{}, fn {name, value}, acc -> - Map.update(acc, name, [value], &(&1 ++ [value])) - end) - end - - async = %Req.Response.Async{ - pid: self(), - ref: ref, - stream_fun: &finch_parse_message/2, - cancel_fun: &finch_cancel/1 - } - - resp = Req.Response.new(status: status, headers: headers) - resp = put_in(resp.body, async) - {req, resp} - end - - defp run_finch_request(finch_request, finch_name, finch_options) do - case Finch.request(finch_request, finch_name, finch_options) do - {:ok, response} -> - Req.Response.new(response) - - {:error, %Mint.TransportError{reason: reason}} -> - %Req.TransportError{reason: reason} - - {:error, %Mint.HTTPError{module: Mint.HTTP1, reason: reason}} -> - %Req.HTTPError{protocol: :http1, reason: reason} - - {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: reason}} -> - %Req.HTTPError{protocol: :http2, reason: reason} - - {:error, %Finch.Error{reason: reason}} -> - %Req.HTTPError{protocol: :http2, reason: reason} - - {:error, exception} -> - exception - end - end - - defp add_private_options(finch_request, nil), do: finch_request - - defp add_private_options(finch_request, private_options) - when is_list(private_options) or is_map(private_options) do - Enum.reduce(private_options, finch_request, fn {k, v}, acc_finch_req -> - Finch.Request.put_private(acc_finch_req, k, v) - end) - end - - defp finch_fields_to_map(fields) do - Enum.reduce(fields, %{}, fn {name, value}, acc -> - Map.update(acc, name, [value], &(&1 ++ [value])) - end) - end - - defp finch_parse_message(ref, {ref, {:data, data}}) do - {:ok, [data: data]} - end - - defp finch_parse_message(ref, {ref, :done}) do - {:ok, [:done]} - end - - defp finch_parse_message(ref, {ref, {:trailers, trailers}}) do - {:ok, [trailers: trailers]} - end - - defp finch_parse_message(ref, {ref, {:error, reason}}) do - {:error, reason} - end - - defp finch_cancel(ref) do - Finch.cancel_async_request(ref) - end - - defp finch_name(request) do - if name = request.options[:finch] do - if request.options[:connect_options] do - raise ArgumentError, "cannot set both :finch and :connect_options" - end - - name - else - connect_options = request.options[:connect_options] || [] - inet6_options = if request.options[:inet6], do: [inet6: true], else: [] - - if connect_options != [] || inet6_options != [] do - Req.Request.validate_options( - connect_options, - MapSet.new([ - :timeout, - :protocols, - :transport_opts, - :proxy_headers, - :proxy, - :client_settings, - :hostname, - - # TODO: Remove on Req v1.0 - :protocol - ]) - ) - - hostname_opts = Keyword.take(connect_options, [:hostname]) - - transport_opts = [ - transport_opts: - Keyword.merge( - Keyword.take(connect_options, [:timeout]) ++ inet6_options, - Keyword.get(connect_options, :transport_opts, []) - ) - ] - - proxy_headers_opts = Keyword.take(connect_options, [:proxy_headers]) - proxy_opts = Keyword.take(connect_options, [:proxy]) - client_settings_opts = Keyword.take(connect_options, [:client_settings]) - - protocols = - cond do - protocols = connect_options[:protocols] -> - protocols - - protocol = connect_options[:protocol] -> - IO.warn([ - "setting `connect_options: [protocol: protocol]` is deprecated, ", - "use `connect_options: [protocols: protocols]` instead" - ]) - - [protocol] - - true -> - Req.Application.__default_protocols__() - end - - pool_opts = [ - conn_opts: - hostname_opts ++ - transport_opts ++ - proxy_headers_opts ++ - proxy_opts ++ - client_settings_opts, - protocols: protocols - ] - - name = - [connect_options, inet6_options] - |> :erlang.term_to_binary() - |> :erlang.md5() - |> Base.url_encode64(padding: false) - - name = Module.concat(Req.FinchSupervisor, "Pool_#{name}") - - case DynamicSupervisor.start_child( - Req.FinchSupervisor, - {Finch, name: name, pools: %{default: pool_opts}} - ) do - {:ok, _} -> - name - - {:error, {:already_started, _}} -> - name - end - else - Req.Finch - end - end + Req.Finch.run(request) end @doc """ diff --git a/test/req/finch_test.exs b/test/req/finch_test.exs new file mode 100644 index 0000000..7c50057 --- /dev/null +++ b/test/req/finch_test.exs @@ -0,0 +1,54 @@ +defmodule Req.FinchTest do + use ExUnit.Case, async: true + + describe "pool_options" do + test "defaults" do + assert Req.Finch.pool_options([]) == + [ + protocols: [:http1] + ] + end + + test "ipv6" do + assert Req.Finch.pool_options(inet6: true) == + [ + protocols: [:http1], + conn_opts: [transport_opts: [inet6: true]] + ] + end + + test "connect_options protocols" do + assert Req.Finch.pool_options(connect_options: [protocols: [:http2]]) == + [ + protocols: [:http2] + ] + end + + test "connect_options timeout" do + assert Req.Finch.pool_options(connect_options: [timeout: 0]) == + [ + protocols: [:http1], + conn_opts: [transport_opts: [timeout: 0]] + ] + end + + test "connect_options transport_opts" do + assert Req.Finch.pool_options(connect_options: [transport_opts: [cacerts: []]]) == + [ + protocols: [:http1], + conn_opts: [transport_opts: [cacerts: []]] + ] + end + + test "connect_options transport_opts + timeout + ipv6" do + assert Req.Finch.pool_options( + connect_options: [timeout: 0, transport_opts: [cacerts: []]], + inet6: true + ) == + [ + protocols: [:http1], + conn_opts: [transport_opts: [timeout: 0, inet6: true, cacerts: []]] + ] + end + end +end