-
Notifications
You must be signed in to change notification settings - Fork 112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Mint.HTTP.recv_response/3 #447
base: main
Are you sure you want to change the base?
Changes from 10 commits
2549e1f
b499e0d
852d90e
347d7e8
e14016a
2c450a7
2c10fca
90142af
7dde3bb
f307e26
55ab096
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -123,6 +123,7 @@ defmodule Mint.HTTP do | |
|
||
alias Mint.{Types, TunnelProxy, UnsafeProxy} | ||
alias Mint.Core.{Transport, Util} | ||
require Util | ||
|
||
@behaviour Mint.Core.Conn | ||
|
||
|
@@ -872,6 +873,148 @@ 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 """ | ||
Sends a request and receives a response from the socket in a blocking way. | ||
|
||
This function is a convenience for sending a request with `request/5` and repeatedly calling `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}`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also trailing headers? |
||
|
||
* `: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. | ||
|
||
Contrary to `recv/3`, this function does not return partial responses on errors. Use | ||
`recv/3` for full control. | ||
|
||
> #### Error {: .error} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We said we'd also raise if the mode is not |
||
> | ||
> This function can only be used for one-off requests. If there is another concurrent request, | ||
> started by `request/5`, it will crash. | ||
|
||
## Options | ||
|
||
* `:timeout` - the maximum amount of time in milliseconds waiting to receive the response. | ||
Setting to `:infinity`, disables the timeout. Defaults to `:infinity`. | ||
Comment on lines
+910
to
+911
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mh, are we ok with a default that potentially blocks forever? Maybe 30s is a better default? |
||
|
||
## Examples | ||
|
||
iex> {:ok, conn} = Mint.HTTP.connect(:https, "httpbin.org", 443, mode: :passive) | ||
iex> {:ok, conn, response} = Mint.HTTP.request_and_response(conn, "GET", "/user-agent", [], nil) | ||
iex> response | ||
%{ | ||
status: 200, | ||
headers: [{"date", ...}, ...], | ||
body: "{\\n \\"user-agent\\": \\"#{@version}\\"\\n}\\n" | ||
} | ||
iex> Mint.HTTP.open?(conn) | ||
true | ||
wojtekmach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
@spec request_and_response( | ||
t(), | ||
method :: String.t(), | ||
path :: String.t(), | ||
Types.headers(), | ||
body :: iodata() | nil | :stream, | ||
options :: [{:timeout, timeout()}] | ||
) :: | ||
{:ok, t(), response} | ||
| {:error, t(), Types.error()} | ||
when response: %{ | ||
status: non_neg_integer(), | ||
headers: [{binary(), binary()}], | ||
body: binary() | ||
wojtekmach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
def request_and_response(conn, method, path, headers, body, options \\ []) do | ||
options = keyword_validate!(options, timeout: :infinity) | ||
|
||
with {:ok, conn, ref} <- request(conn, method, path, headers, body) do | ||
recv_response(conn, ref, options[:timeout]) | ||
end | ||
end | ||
|
||
defp recv_response(conn, request_ref, timeout) when Util.is_timeout(timeout) do | ||
recv_response([], {nil, [], ""}, conn, request_ref, timeout) | ||
end | ||
|
||
defp recv_response( | ||
[{:status, ref, new_status} | rest], | ||
{_status, headers, body}, | ||
conn, | ||
ref, | ||
timeout | ||
) do | ||
recv_response(rest, {new_status, headers, body}, conn, ref, timeout) | ||
end | ||
|
||
defp recv_response( | ||
[{:headers, ref, new_headers} | rest], | ||
{status, headers, body}, | ||
conn, | ||
ref, | ||
timeout | ||
) do | ||
recv_response(rest, {status, headers ++ new_headers, body}, conn, ref, timeout) | ||
end | ||
|
||
defp recv_response([{:data, ref, data} | rest], {status, headers, body}, conn, ref, timeout) do | ||
recv_response(rest, {status, headers, [body, data]}, conn, ref, timeout) | ||
end | ||
|
||
defp recv_response([{:done, ref} | _rest], {status, headers, body}, conn, ref, _timeout) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we error out here if there is trailing response data? That is, this should always be |
||
response = %{status: status, headers: headers, body: IO.iodata_to_binary(body)} | ||
{:ok, conn, response} | ||
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) when is_tuple(entry) do | ||
ref = elem(entry, 1) | ||
raise "received unexpected response from request #{inspect(ref)}" | ||
end | ||
|
||
defp recv_response([], acc, conn, ref, timeout) do | ||
start_time = System.monotonic_time(:millisecond) | ||
|
||
case recv(conn, 0, timeout) do | ||
{:ok, conn, entries} -> | ||
timeout = | ||
if is_integer(timeout) do | ||
timeout - System.monotonic_time(:millisecond) - start_time | ||
else | ||
timeout | ||
end | ||
|
||
recv_response(entries, acc, conn, ref, timeout) | ||
|
||
{:error, conn, reason, _responses} -> | ||
{:error, conn, reason} | ||
end | ||
end | ||
|
||
# TODO: Remove when we require Elixir v1.13 | ||
if Version.match?(System.version(), ">= 1.13.0") do | ||
wojtekmach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
defp keyword_validate!(keyword, values), do: Keyword.validate!(keyword, values) | ||
else | ||
defp keyword_validate!(keyword, _values), do: keyword | ||
end | ||
|
||
@doc """ | ||
Changes the mode of the underlying socket. | ||
|
||
|
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: [request_and_response: 6] | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.