Skip to content
3 changes: 0 additions & 3 deletions lib/tesla/mock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,6 @@ defmodule Tesla.Mock do

{:error, {:already_started, pid}} ->
Agent.update(pid, fn _ -> fun end)

other ->
raise other
end
end

Expand Down
1 change: 0 additions & 1 deletion lib/tesla/opentelemetry/sem_conv.ex
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ if Code.ensure_loaded?(OpenTelemetry.SemConv.HTTPAttributes) do
end

defp extract_port(%URI{port: port}) when is_integer(port), do: port
defp extract_port(%URI{scheme: "https"}), do: 443
defp extract_port(_), do: 80

defp error_type_string(%{__struct__: struct}), do: inspect(struct)
Expand Down
1 change: 0 additions & 1 deletion lib/tesla/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ defmodule Tesla.Test do

defp encode!(body, "application/json") when is_binary(body), do: body
defp encode!(body, "application/json"), do: Jason.encode!(body)
defp encode!(body, _), do: body

defp read_body!(%Tesla.Env{} = env) do
case Tesla.get_headers(env, "content-type") do
Expand Down
48 changes: 48 additions & 0 deletions test/tesla/adapter/finch_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ defmodule Tesla.Adapter.FinchTest do
)
end

test "raises on an unknown :response option" do
assert_raise RuntimeError, ~r/Unknown response option: :bogus/, fn ->
call(%Env{method: :get, url: "#{@http}/ip"}, response: :bogus)
end
end

test "surfaces the protocol error hidden behind the Finch wrapper" do
url = start_raw_server(fn socket -> :gen_tcp.send(socket, "NOT-HTTP garbage\r\n\r\n") end)

assert {:error, %Mint.HTTPError{reason: :invalid_status_line}} =
call(%Env{method: :get, url: url}, receive_timeout: 500)
end

describe "streamed response failures" do
test "raises with the transport reason when the connection drops mid stream" do
url = start_chunked_server(fn socket -> :gen_tcp.close(socket) end)
Expand All @@ -84,6 +97,18 @@ defmodule Tesla.Adapter.FinchTest do
assert_raise Tesla.Error, ~r/^:timeout /, fn -> Enum.to_list(env.body) end
end

test "ends the stream without raising when the response carries trailers" do
url =
start_chunked_server(fn socket ->
:gen_tcp.send(socket, "0\r\nx-checksum: abc123\r\n\r\n")
end)

assert {:ok, env} =
call(%Env{method: :get, url: url}, response: :stream, receive_timeout: 200)

assert Enum.to_list(env.body) == ["hello"]
end

test "ends the stream without raising when the response completes" do
url = start_chunked_server(fn socket -> :gen_tcp.send(socket, "0\r\n\r\n") end)

Expand All @@ -94,6 +119,29 @@ defmodule Tesla.Adapter.FinchTest do
end
end

defp start_raw_server(respond) do
{:ok, listen} =
:gen_tcp.listen(0,
ip: {127, 0, 0, 1},
mode: :binary,
packet: :raw,
active: false,
reuseaddr: true
)

{:ok, port} = :inet.port(listen)

spawn_link(fn ->
{:ok, socket} = :gen_tcp.accept(listen, 5_000)
{:ok, _request} = :gen_tcp.recv(socket, 0, 5_000)

respond.(socket)
Process.sleep(500)
end)

"http://127.0.0.1:#{port}/"
end

# Sends a chunked response with a single "hello" chunk, then hands the socket
# over to `after_chunk` to decide how the response ends.
defp start_chunked_server(after_chunk) do
Expand Down
110 changes: 110 additions & 0 deletions test/tesla/adapter/gun_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,31 @@ defmodule Tesla.Adapter.GunTest do
new_url = "http://127.0.0.1:#{Application.get_env(:httparrot, :http_port)}/stream-bytes/10"
assert {:error, :invalid_conn} = call(Map.put(request, :url, new_url), conn: conn)
end

test "opened to another port", %{request: request, conn: conn} do
uri = URI.parse(@https)
new_url = "http://#{uri.host}:#{uri.port}/stream-bytes/10"

assert {:error, :invalid_conn} = call(Map.put(request, :url, new_url), conn: conn)
end

test "opened for another scheme on the same host and port", %{request: request} do
uri = URI.parse(@https)

tls_opts_key = if @gun2, do: :tls_opts, else: :transport_opts

{:ok, conn} =
:gun.open(to_charlist(uri.host), uri.port, %{
:transport => :tls,
tls_opts_key => [verify: :verify_none]
})

on_exit(fn -> Gun.close(conn) end)

new_url = "http://#{uri.host}:#{uri.port}/stream-bytes/10"

assert {:error, :invalid_conn} = call(Map.put(request, :url, new_url), conn: conn)
end
end

test "error response" do
Expand Down Expand Up @@ -378,6 +403,91 @@ defmodule Tesla.Adapter.GunTest do
Gun.close(pid)
end

test "relays a stream error to the request owner, the reply_to and the stream owner" do
test_pid = self()

url =
start_raw_server(fn socket ->
:gen_tcp.send(socket, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")
Process.sleep(50)
:gen_tcp.close(socket)
end)

stream_owner = spawn_forwarder(test_pid, :stream_owner)
on_exit(fn -> Process.exit(stream_owner, :kill) end)

request = %Env{
method: :get,
url: url,
private: %{tesla_gun_stream_owner: stream_owner}
}

reply_to = fn message -> send(test_pid, {:gun_reply_to, message}) end

assert {:ok, %Env{status: 200, body: %{pid: pid, stream: stream}}} =
call(request, body_as: :chunks, reply_to: reply_to, timeout: 2_000)

assert_receive {:gun_error, ^pid, ^stream, reason}, 1_000
assert_receive {:gun_reply_to, {:gun_error, ^pid, ^stream, ^reason}}, 1_000
assert_receive {:stream_owner, {:gun_error, ^pid, ^stream, ^reason}}, 1_000

Gun.close(pid)
end

test "reports the monitor reason when a process goes down while the body is read" do
victim = spawn(fn -> Process.sleep(:infinity) end)
Process.monitor(victim)

url =
start_raw_server(fn socket ->
:gen_tcp.send(socket, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")
Process.sleep(150)
Process.exit(victim, :kill)
Process.sleep(2_000)
end)

request = %Env{method: :get, url: url}

assert {:error, :killed} == call(request, timeout: 2_000)
end

test "keeps waiting for the response while the connection goes down and back up" do
url = start_raw_server(fn socket -> :gen_tcp.close(socket) end, accept: :forever)

request = %Env{method: :get, url: url}

assert {:error, :recv_response_timeout} ==
call(request, timeout: 1_500, retry: 5, retry_timeout: 50)
end

defp start_raw_server(on_request, opts \\ []) do
{:ok, listen_socket} =
:gen_tcp.listen(0, [:binary, packet: :raw, active: false, reuseaddr: true])

{:ok, port} = :inet.port(listen_socket)

server =
spawn(fn ->
accept = fn accept ->
with {:ok, socket} <- :gen_tcp.accept(listen_socket),
{:ok, _request} <- :gen_tcp.recv(socket, 0, 5_000) do
on_request.(socket)
end

if opts[:accept] == :forever, do: accept.(accept)
end

accept.(accept)
end)

on_exit(fn ->
Process.exit(server, :kill)
:gen_tcp.close(listen_socket)
end)

"http://127.0.0.1:#{port}/raw"
end

defp spawn_forwarder(test_pid, tag) do
spawn(fn -> forward_messages(test_pid, tag) end)
end
Expand Down
8 changes: 8 additions & 0 deletions test/tesla/adapter/httpc_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,12 @@ defmodule Tesla.Adapter.HttpcTest do

assert data["headers"]["content-type"] == "text/plain"
end

test "binary body format is passed through untouched" do
env = %Env{method: :get, url: "#{@http}/base64/dGVzbGE"}

assert {:ok, %Env{} = response} = call(env, body_format: :binary)
assert response.status == 200
assert response.body == "tesla"
end
end
8 changes: 8 additions & 0 deletions test/tesla/adapter/ibrowse_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ defmodule Tesla.Adapter.IbrowseTest do

assert {:error, :method_not_supported_by_adapter} = call(env)
end

test "binary response format is passed through untouched" do
env = %Env{method: :get, url: "#{@http}/base64/dGVzbGE"}

assert {:ok, %Env{} = response} = call(env, response_format: :binary)
assert response.status == 200
assert response.body == "tesla"
end
end
34 changes: 34 additions & 0 deletions test/tesla/builder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,37 @@ defmodule Tesla.BuilderTest do
end
end
end

defmodule Tesla.BuilderDeprecationTest do
use ExUnit.Case, async: false

import ExUnit.CaptureIO

setup do
previous = Application.get_env(:tesla, :disable_deprecated_builder_warning)
Application.put_env(:tesla, :disable_deprecated_builder_warning, false)

on_exit(fn ->
case previous do
nil -> Application.delete_env(:tesla, :disable_deprecated_builder_warning)
value -> Application.put_env(:tesla, :disable_deprecated_builder_warning, value)
end
end)

:ok
end

test "use Tesla warns that the builder is soft-deprecated" do
output =
capture_io(:stderr, fn ->
Code.eval_string("""
defmodule Tesla.BuilderDeprecationTest.WarnedClient do
use Tesla
end
""")
end)

assert output =~ "are soft-deprecated"
assert output =~ "disable_deprecated_builder_warning: true"
end
end
12 changes: 12 additions & 0 deletions test/tesla/middleware/decode_rels_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,16 @@ defmodule Tesla.Middleware.DecodeRelsTest do

assert env.opts[:rels] == nil
end

defmodule ErrorClient do
use Tesla

plug Tesla.Middleware.DecodeRels

adapter fn _env -> {:error, :econnrefused} end
end

test "passes an adapter error through untouched" do
assert {:error, :econnrefused} = ErrorClient.get("/rels")
end
end
28 changes: 28 additions & 0 deletions test/tesla/middleware/follow_redirects_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,32 @@ defmodule Tesla.Middleware.FollowRedirectsTest do
assert_receive [{"Content-Type", "application/json"}]
end
end

describe "responses that are not followed" do
defmodule NoRedirectsErrorClient do
use Tesla

plug Tesla.Middleware.FollowRedirects, max_redirects: 0

adapter fn _env -> {:error, :econnrefused} end
end

defmodule MissingLocationClient do
use Tesla

plug Tesla.Middleware.FollowRedirects

adapter fn env -> {:ok, %{env | status: 301, headers: [], body: "no location"}} end
end

test "passes an adapter error through untouched when no redirects are left" do
assert {:error, :econnrefused} = NoRedirectsErrorClient.get("http://example.com/")
end

test "returns the response when a redirect has no location header" do
assert {:ok, env} = MissingLocationClient.get("http://example.com/")
assert env.status == 301
assert env.body == "no location"
end
end
end
27 changes: 27 additions & 0 deletions test/tesla/middleware/form_urlencoded_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,33 @@ defmodule Tesla.Middleware.FormUrlencodedTest do
end
end

describe "responses the middleware leaves alone" do
defmodule ErrorClient do
use Tesla

plug Tesla.Middleware.FormUrlencoded

adapter fn _env -> {:error, :econnrefused} end
end

defmodule NoContentTypeClient do
use Tesla

plug Tesla.Middleware.FormUrlencoded

adapter fn env -> {:ok, %{env | status: 200, headers: [], body: "x=1&y=2"}} end
end

test "passes an adapter error through untouched" do
assert {:error, :econnrefused} = ErrorClient.get("/")
end

test "leaves the body alone when the response has no content-type" do
assert {:ok, env} = NoContentTypeClient.get("/")
assert env.body == "x=1&y=2"
end
end

describe "Encode / Decode" do
defmodule EncodeDecodeFormUrlencodedClient do
use Tesla
Expand Down
Loading
Loading