Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach committed Jul 31, 2024
1 parent cc9a142 commit a561dcf
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 92 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ services:

httpbin:
image: docker.io/kennethreitz/httpbin:latest
platform: linux/amd64
ports:
- "8080:80"

Expand Down
3 changes: 3 additions & 0 deletions lib/mint/core/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ defmodule Mint.Core.Util do

alias Mint.Types

defguard is_timeout(timeout)
when (is_integer(timeout) and timeout >= 0) or timeout == :infinity

@spec hostname(keyword(), String.t()) :: String.t()
def hostname(opts, address) when is_list(opts) do
case Keyword.fetch(opts, :hostname) do
Expand Down
95 changes: 95 additions & 0 deletions lib/mint/http.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ defmodule Mint.HTTP do

alias Mint.{Types, TunnelProxy, UnsafeProxy}
alias Mint.Core.{Transport, Util}
require Util

@behaviour Mint.Core.Conn

Expand Down Expand Up @@ -872,6 +873,100 @@ defmodule Mint.HTTP do
| {:error, t(), Types.error(), [Types.response()]}
def recv(conn, byte_count, timeout), do: conn_module(conn).recv(conn, byte_count, timeout)

@version Mix.Project.config()[:version]

@doc """
Receives response from the socket in a blocking way.
This function receives a response for the request identified by `request_ref` on the connection
`conn`.
`timeout` is the maximum time to wait before returning an error.
This function is a convenience for repeatedly calling `Mint.HTTP.recv/3`. The result is either:
* `{:ok, conn, response}` where `conn` is the updated connection and `response` is a map
with the following keys:
* `:status` - HTTP response status, an integer.
* `:headers` - HTTP response headers, a list of tuples `{header_name, header_value}`.
* `:body` - HTTP response body, a binary.
* `{:error, conn, reason}` where `conn` is the updated connection and `reason` is the cause
of the error. It is important to store the returned connection over the old connection in
case of errors too, because the state of the connection might change when there are errors
as well. An error when sending a request **does not** necessarily mean that the connection
is closed. Use `open?/1` to verify that the connection is open.
## Examples
iex> {:ok, conn} = Mint.HTTP.connect(:https, "httpbin.org", 443, mode: :passive)
iex> {:ok, conn, request_ref} = Mint.HTTP.request(conn, "GET", "/user-agent", [], nil)
iex> {:ok, _conn, response} = Mint.HTTP.recv_response(conn, request_ref, 5000)
iex> response
%{
status: 200,
headers: [{"date", ...}, ...],
body: "{\\n \\"user-agent\\": \\"#{@version}\\"\\n}\\n"
}
"""
@spec recv_response(t(), Types.request_ref(), timeout()) ::
{:ok, t(), response} | {:error, Types.error()}
when response: %{
status: non_neg_integer(),
headers: [{binary(), binary()}],
body: binary()
}
def recv_response(conn, ref, timeout)
when is_reference(ref) and Util.is_timeout(timeout) do
recv_response([], %{status: nil, headers: [], body: ""}, conn, ref, timeout)
end

defp recv_response([{:status, ref, status} | rest], acc, conn, ref, timeout) do
acc = put_in(acc.status, status)
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([{:headers, ref, headers} | rest], acc, conn, ref, timeout) do
acc = update_in(acc.headers, &(&1 ++ headers))
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([{:data, ref, data} | rest], acc, conn, ref, timeout) do
acc = update_in(acc.body, &(&1 <> data))
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([{:done, ref} | _rest], acc, conn, ref, _timeout) do
{:ok, conn, acc}
end

defp recv_response([{:error, ref, error} | _rest], _acc, conn, ref, _timeout) do
{:error, conn, error}
end

# Ignore entries from other requests.
defp recv_response([_entry | rest], acc, conn, ref, timeout) do
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([], acc, conn, ref, timeout) do
start_time = System.monotonic_time(:millisecond)

with {:ok, conn, entries} <- recv(conn, 0, timeout) do
timeout =
if is_integer(timeout) do
timeout - System.monotonic_time(:millisecond) - start_time
else
timeout
end

recv_response(entries, acc, conn, ref, timeout)
end
end

@doc """
Changes the mode of the underlying socket.
Expand Down
61 changes: 0 additions & 61 deletions lib/mint/http1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -529,67 +529,6 @@ defmodule Mint.HTTP1 do
"Use Mint.HTTP.set_mode/2 to set the connection to passive mode"
end

@doc """
TODO
## Examples
iex> {:ok, conn} = Mint.HTTP1.connect(:https, "httpbin.org", 443, mode: :passive)
iex> {:ok, conn, ref} = Mint.HTTP1.request(conn, "GET", "/status/user-agent", [], nil)
iex> {:ok, _conn, response} = Mint.HTTP1.recv_response(conn, ref, 5000)
iex> response
%{
status: 201,
headers: [
{"date", ...},
...
],
body: "{\\n \\"user-agent\\": \\"mint/1.6.2\\"\\n}\\n"
}
"""
def recv_response(conn, ref, timeout) do
recv_response([], %{status: nil, headers: [], body: ""}, conn, ref, timeout)
end

defp recv_response([{:status, ref, status} | rest], acc, conn, ref, timeout) do
acc = put_in(acc.status, status)
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([{:headers, ref, headers} | rest], acc, conn, ref, timeout) do
acc = update_in(acc.headers, &(&1 ++ headers))
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([{:data, ref, data} | rest], acc, conn, ref, timeout) do
acc = update_in(acc.body, &(&1 <> data))
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([{:done, ref} | _], acc, conn, ref, _timeout) do
{:ok, conn, acc}
end

# Ignore entries from other requests.
defp recv_response([_entry | rest], acc, conn, ref, timeout) do
recv_response(rest, acc, conn, ref, timeout)
end

defp recv_response([], acc, conn, ref, timeout) do
start_time = System.monotonic_time(:millisecond)

with {:ok, conn, entries} <- recv(conn, 0, timeout) do
timeout =
if is_integer(timeout) do
timeout - System.monotonic_time(:millisecond) - start_time
else
timeout
end

recv_response(entries, acc, conn, ref, timeout)
end
end

@doc """
See `Mint.HTTP.set_mode/2`.
"""
Expand Down
2 changes: 1 addition & 1 deletion test/http_test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Mint.HTTPTest do
use ExUnit.Case, async: true
doctest Mint.HTTP
doctest Mint.HTTP, except: [recv_response: 3]
end
52 changes: 23 additions & 29 deletions test/mint/http1/conn_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,9 @@ defmodule Mint.HTTP1Test do

require Mint.HTTP

test "foo" do
{:ok, _} =
Bandit.start_link(
port: 4000,
plug: fn conn, _ ->
Plug.Conn.send_resp(conn, 200, "hi")
end,
startup_log: false
)

{:ok, conn} = HTTP1.connect(:http, "localhost", 4000, mode: :passive)
{:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)
{:ok, conn, response} = HTTP1.recv_response(conn, ref, 5000)

assert %{
status: 200,
headers: [
{"date", _},
{"content-length", "2"},
{"vary", "accept-encoding"},
{"cache-control", "max-age=0, private, must-revalidate"}
],
body: "hi"
} = response

assert HTTP1.open?(conn)
end

setup do
{:ok, port, server_ref} = TestServer.start()
assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, mode: :passive)
assert {:ok, conn} = HTTP1.connect(:http, "localhost", port)
assert_receive {^server_ref, server_socket}

[conn: conn, port: port, server_ref: server_ref, server_socket: server_socket]
Expand Down Expand Up @@ -488,6 +460,28 @@ defmodule Mint.HTTP1Test do
assert responses == [{:status, ref, 200}]
end

test "starting a connection in :passive mode and using Mint.HTTP.recv_response/3",
%{port: port, server_ref: server_ref} do
assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, mode: :passive)
assert_receive {^server_ref, server_socket}

{:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)

:ok = :gen_tcp.send(server_socket, "HTTP/1.1 200 OK\r\n")
:ok = :gen_tcp.send(server_socket, "content-type: text/plain\r\n")
:ok = :gen_tcp.send(server_socket, "content-length: 10\r\n\r\n")
:ok = :gen_tcp.send(server_socket, "hello")
:ok = :gen_tcp.send(server_socket, "world")

assert {:ok, _conn, response} = Mint.HTTP.recv_response(conn, ref, 100)

assert response == %{
body: "helloworld",
headers: [{"content-type", "text/plain"}, {"content-length", "10"}],
status: 200
}
end

test "changing the connection mode with set_mode/2",
%{conn: conn, server_socket: server_socket} do
assert_raise ArgumentError, ~r"can't use recv/3", fn ->
Expand Down
45 changes: 44 additions & 1 deletion test/mint/http2/conn_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,11 @@ defmodule Mint.HTTP2Test do
hbf: hbf,
flags: set_flags(:headers, [:end_headers])
),
data(stream_id: stream_id, data: "some data", flags: set_flags(:data, [])),
data(
stream_id: stream_id,
data: "some data",
flags: set_flags(:data, [])
),
headers(
stream_id: stream_id,
hbf: trailer_hbf1,
Expand Down Expand Up @@ -2083,6 +2087,45 @@ defmodule Mint.HTTP2Test do
assert HTTP2.open?(conn)
end

@tag connect_options: [mode: :passive]
test "starting a connection with :passive mode and using Mint.HTTP.recv_response/3", %{
conn: conn
} do
{conn, ref} = open_request(conn)

assert_recv_frames [headers(stream_id: stream_id)]

data =
server_encode_frames([
headers(
stream_id: stream_id,
hbf:
server_encode_headers([
{":status", "200"},
{"content-type", "text/plain"},
{"content-length", "10"}
]),
flags: set_flags(:headers, [:end_headers])
),
data(
stream_id: stream_id,
data: "helloworld",
flags: set_flags(:data, [:end_stream])
)
])

:ok = :ssl.send(server_get_socket(), data)
assert {:ok, _conn, response} = Mint.HTTP.recv_response(conn, ref, 100)

assert response == %{
body: "helloworld",
headers: [{"content-type", "text/plain"}, {"content-length", "10"}],
status: 200
}

assert HTTP2.open?(conn)
end

test "changing the mode of a connection with set_mode/2", %{conn: conn} do
assert_raise ArgumentError, ~r"^can't use recv/3", fn ->
HTTP2.recv(conn, 0, 100)
Expand Down

0 comments on commit a561dcf

Please sign in to comment.