Skip to content
Draft
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
84 changes: 76 additions & 8 deletions lib/tesla/adapter/hackney.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,71 @@

- `:max_body` - Max response body size in bytes. Actual response may be bigger because hackney stops after the last chunk that surpasses `:max_body`.
"""
@hackney_version Application.spec(:hackney, :vsn)
|> to_string()
|> Version.parse!()
@behaviour Tesla.Adapter
alias Tesla.Multipart

# hackney 1.x uses references while hackney 2.x uses pids
# https://github.com/benoitc/hackney/blob/master/guides/MIGRATION.md#connection-handle
# further usage in code is the same
defguard is_hackney_connection_handle(handle) when is_reference(handle) or is_pid(handle)

@impl Tesla.Adapter
def call(env, opts) do
opts = process_options(opts)

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

# Hackney 3.X sets cacerts from certifi by default, which causes cacertfile to be ignored
# Convert cacertfile to cacerts to fix SSL with custom CA certificates
if Version.match?(@hackney_version, "~> 3.0") do
defp process_options(opts) do
process_ssl_options(opts)
end

defp process_ssl_options(opts) do
case Keyword.get(opts, :ssl_options) do
nil ->
opts

ssl_opts ->
case Keyword.get(ssl_opts, :cacertfile) do
nil ->
opts

cacertfile ->
# Read and parse CA cert file
{:ok, pem_data} = File.read(cacertfile)
pem_entries = :public_key.pem_decode(pem_data)

Check warning on line 72 in lib/tesla/adapter/hackney.ex

View workflow job for this annotation

GitHub Actions / Dialyzer

unknown_function

Function :public_key.pem_decode/1 does not exist.
cacerts = Enum.map(pem_entries, fn {_type, der, _} -> der end)

# Replace cacertfile with cacerts
ssl_opts =
ssl_opts
|> Keyword.delete(:cacertfile)
|> Keyword.put(:cacerts, cacerts)

Keyword.put(opts, :ssl_options, ssl_opts)
end
end
end
else
defp process_options(opts), do: opts
end

defp format_headers(headers) do
for {key, value} <- headers do
{String.downcase(to_string(key)), to_string(value)}
end
end

defp format_body(data) when is_list(data), do: IO.iodata_to_binary(data)
defp format_body(data) when is_binary(data) or is_reference(data), do: data
defp format_body(data) when is_binary(data) or is_hackney_connection_handle(data), do: data

defp request(env, opts) do
request(
Expand Down Expand Up @@ -78,8 +125,12 @@
defp request_stream(method, url, headers, body, opts) do
with {:ok, ref} <- :hackney.request(method, url, headers, :stream, opts) do
case send_stream(ref, body) do
:ok -> handle(:hackney.start_response(ref), opts)
error -> handle(error, opts)
:ok ->
:hackney.finish_send_body(ref)

Check warning on line 129 in lib/tesla/adapter/hackney.ex

View workflow job for this annotation

GitHub Actions / Dialyzer

call

The function call finish_send_body will not succeed.
handle(:hackney.start_response(ref), opts)

error ->
handle(error, opts)
end
else
e -> handle(e, opts)
Expand All @@ -87,8 +138,8 @@
end

defp send_stream(ref, body) do
Enum.reduce_while(body, :ok, fn data, _ ->

Check warning on line 141 in lib/tesla/adapter/hackney.ex

View workflow job for this annotation

GitHub Actions / Dialyzer

no_return

The created anonymous function has no local return.
case :hackney.send_body(ref, data) do

Check warning on line 142 in lib/tesla/adapter/hackney.ex

View workflow job for this annotation

GitHub Actions / Dialyzer

call

The function call send_body will not succeed.
:ok -> {:cont, :ok}
error -> {:halt, error}
end
Expand All @@ -99,13 +150,30 @@
defp handle({:error, _} = error, _opts), do: error
defp handle({:ok, status, headers}, _opts), do: {:ok, status, headers, []}

defp handle({:ok, ref}, _opts) when is_reference(ref) do
handle_async_response({ref, %{status: nil, headers: nil}})
defp handle({:ok, handle}, _opts) when is_hackney_connection_handle(handle) do
handle_async_response({handle, %{status: nil, headers: nil}})
end

defp handle({:ok, status, headers, ref}, opts) when is_reference(ref) do
with {:ok, body} <- :hackney.body(ref, Keyword.get(opts, :max_body, :infinity)) do
{:ok, status, headers, body}
if Version.match?(@hackney_version, "~> 1.0") do
# Hackney 1.x: uses :hackney.body/2 with max_body parameter
defp handle({:ok, status, headers, handle}, opts)
when is_hackney_connection_handle(handle) do
with {:ok, body} <- :hackney.body(handle, Keyword.get(opts, :max_body, :infinity)) do
{:ok, status, headers, body}
end
end
end

if Version.match?(@hackney_version, "~> 3.0") do
# Hackney 3.x: for streaming requests, :hackney.start_response returns handle as PID
# Must use :hackney_conn.body/2 to read body with timeout
defp handle({:ok, status, headers, handle}, opts)
when is_hackney_connection_handle(handle) do
timeout = Keyword.get(opts, :recv_timeout, :infinity)

with {:ok, body} <- :hackney_conn.body(handle, timeout) do
{:ok, status, headers, body}
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ defmodule Tesla.Mixfile do

# http clients
{:ibrowse, "4.4.2", optional: true},
{:hackney, "~> 1.21", optional: true},
{:hackney, ">= 3.2.1", optional: true},
{:gun, ">= 1.0.0", optional: true},
{:finch, "~> 0.13", optional: true},
{:castore, "~> 0.1 or ~> 1.0", optional: true},
Expand Down
7 changes: 4 additions & 3 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"certifi": {:hex, :certifi, "2.16.0", "a4edfc1d2da3424d478a3271133bf28e0ec5e6fd8c009aab5a4ae980cb165ce9", [:rebar3], [], "hexpm", "8a64f6669d85e9cc0e5086fcf29a5b13de57a13efa23d3582874b9a19303f184"},
"con_cache": {:hex, :con_cache, "1.1.0", "45c7c6cd6dc216e47636232e8c683734b7fe293221fccd9454fa1757bc685044", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8655f2ae13a1e56c8aef304d250814c7ed929c12810f126fc423ecc8e871593b"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
Expand All @@ -16,11 +16,11 @@
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"fuse": {:hex, :fuse, "2.5.0", "71afa90be21da4e64f94abba9d36472faa2d799c67fedc3bd1752a88ea4c4753", [:rebar3], [], "hexpm", "7f52a1c84571731ad3c91d569e03131cc220ebaa7e2a11034405f0bac46a4fef"},
"gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
"hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"},
"hackney": {:hex, :hackney, "3.2.1", "600fba0d2a5d6b20f2bc2aa90a3f3e082f18cac50d648cde3bdef501a36a5090", [:rebar3], [{:certifi, "~> 2.16.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 0.10.2", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "1d9260c31b7c910b63e6b3929d296f18ba3d8217ef01800989afcfcc77776afe"},
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
"httparrot": {:hex, :httparrot, "1.4.0", "ba4b9ff1199b409349a887a683b7d4c1a5f1357d654efde9b6f6a07984efc1f9", [:mix], [{:con_cache, "~> 1.1", [hex: :con_cache, repo: "hexpm", optional: false]}, {:cowboy, "~> 2.12", [hex: :cowboy, repo: "hexpm", optional: false]}, {:exjsx, "~> 3.0 or ~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}], "hexpm", "6ebdcf36353424ada74743fe3180cb5f5ed5424349fc41a425521b2bf5678081"},
"ibrowse": {:hex, :ibrowse, "4.4.2", "7fe943ba6cb88514dca631c7408c4a7897ce69fb0905244e2bfbc839d42f8d45", [:rebar3], [], "hexpm", "f088cee1faf6514b18c7783e8bc64c628d140a239786dc1f58fe9766e9584f41"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
"inch_ex": {:hex, :inch_ex, "2.1.0", "0a6349fa71b89a96851da6bc4dcba304f74e23b66353fe47a939722b0a2ea539", [:mix], [{:bunt, "~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "bf6fbaac4dca8cbe95db58a213608c41f2d3fe404cc608b961ffb11c60190111"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"},
Expand All @@ -43,6 +43,7 @@
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"poison": {:hex, :poison, "6.0.0", "9bbe86722355e36ffb62c51a552719534257ba53f3271dacd20fbbd6621a583a", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "bb9064632b94775a3964642d6a78281c07b7be1319e0016e1643790704e739a2"},
"quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
Expand Down
14 changes: 1 addition & 13 deletions test/tesla/adapter/hackney_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,7 @@ defmodule Tesla.Adapter.HackneyTest do

assert {:ok, %Env{} = response} = call(request, with_body: true, async: true)
assert response.status == 200
assert is_reference(response.body) == true
end

test "get with `:max_body` option" do
request = %Env{
method: :post,
url: "#{@http}/post",
body: String.duplicate("long response", 1000)
}

assert {:ok, %Env{} = response} = call(request, with_body: true, max_body: 100)
assert response.status == 200
assert byte_size(response.body) < 2000
assert is_reference(response.body) or is_pid(response.body) == true
end

test "request timeout error" do
Expand Down
Loading