Skip to content
Merged
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
6 changes: 6 additions & 0 deletions lib/mint/http.ex
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,12 @@ defmodule Mint.HTTP do
You can have zero or more `1xx` `:status` and `:headers` responses for a
single request, but they all precede a single non-`1xx` `:status` response.

* `{:status_reason, request_ref, reason_phrase}` - returned when the server replied
with a response status code and a reason-phrase. The reason-phrase is a string.
Returned when the `:optional_responses` option is passed to `connect/4`, with
`:status_reason` in the list. See `Mint.HTTP1.connect/4` for more information.
This is only available for HTTP/1.1 connections. *Available since v1.7.2*.

* `{:headers, request_ref, headers}` - returned when the server replied
with a list of headers. Headers are in the form `{header_name, header_value}`
with `header_name` and `header_value` being strings. A single `:headers` response
Expand Down
47 changes: 43 additions & 4 deletions lib/mint/http1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ defmodule Mint.HTTP1 do
"""
@type error_reason() :: term()

@optional_responses_opts [:status_reason]

defstruct [
:host,
:port,
Expand All @@ -99,7 +101,8 @@ defmodule Mint.HTTP1 do
buffer: "",
proxy_headers: [],
private: %{},
log: false
log: false,
optional_responses: []
]

defmacrop log(conn, level, message) do
Expand Down Expand Up @@ -128,6 +131,12 @@ defmodule Mint.HTTP1 do
will not be validated. You might want this if you deal with non standard-
conforming URIs but need to preserve them. The default is to validate the request
target. *Available since v1.7.0*.
* `:optional_responses` - (list of atoms) a list of optional responses to return.
The possible values in the list are -
* `:status_reason` which will return the
[reason-phrase](https://datatracker.ietf.org/doc/html/rfc9112#name-status-line)
for the status code, if it is returned by the server in status-line.
This is only available for HTTP/1.1 connections. *Available since v1.7.2*.

"""
@spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) ::
Expand Down Expand Up @@ -206,7 +215,8 @@ defmodule Mint.HTTP1 do
state: :open,
log: log?,
case_sensitive_headers: Keyword.get(opts, :case_sensitive_headers, false),
skip_target_validation: Keyword.get(opts, :skip_target_validation, false)
skip_target_validation: Keyword.get(opts, :skip_target_validation, false),
optional_responses: validate_optional_response_values(opts)
}

{:ok, conn}
Expand All @@ -217,6 +227,21 @@ defmodule Mint.HTTP1 do
end
end

defp validate_optional_response_values(opts) do
opts
|> Keyword.get(:optional_responses, [])
|> Enum.map(fn opt ->
if opt not in @optional_responses_opts do
raise ArgumentError, """
invalid :optional_responses value #{opt}.
allowed values are - #{inspect(@optional_responses_opts)}
"""
end

opt
end)
end

@doc """
See `Mint.HTTP.close/1`.
"""
Expand Down Expand Up @@ -646,10 +671,10 @@ defmodule Mint.HTTP1 do

defp decode(:status, %{request: request} = conn, data, responses) do
case Response.decode_status_line(data) do
{:ok, {version, status, _reason}, rest} ->
{:ok, {version, status, _reason} = status_line, rest} ->
request = %{request | version: version, status: status, state: :headers}
conn = %{conn | request: request}
responses = [{:status, request.ref, status} | responses]
responses = put_status_responses(conn, status_line, responses)
decode(:headers, conn, rest, responses)

:more ->
Expand Down Expand Up @@ -872,6 +897,20 @@ defmodule Mint.HTTP1 do
end
end

defp put_status_responses(
%{request: request, optional_responses: optional_responses},
{_version, status, reason},
responses
) do
responses = [{:status, request.ref, status} | responses]

if Enum.member?(optional_responses, :status_reason) do
[{:status_reason, request.ref, reason} | responses]
else
responses
end
end

defp store_header(%{content_length: nil} = request, "content-length", value) do
with {:ok, content_length} <- Parse.content_length_header(value),
do: {:ok, %{request | content_length: content_length}}
Expand Down
39 changes: 39 additions & 0 deletions test/mint/http1/conn_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ defmodule Mint.HTTP1Test do
refute HTTP1.open?(conn)
end

test "raises error connecting with invalid optional_responses params", %{port: port} do
assert_raise ArgumentError, fn ->
HTTP1.connect(:http, "localhost", port, optional_responses: [:someting])
end
end

test "pipeline", %{conn: conn} do
{:ok, conn, ref1} = HTTP1.request(conn, "GET", "/", [], nil)
{:ok, conn, ref2} = HTTP1.request(conn, "GET", "/", [], nil)
Expand Down Expand Up @@ -581,6 +587,39 @@ defmodule Mint.HTTP1Test do
end
end

describe "status reason" do
setup %{port: port} do
assert {:ok, conn} =
HTTP1.connect(:http, "localhost", port, optional_responses: [:status_reason])

[conn: conn]
end

test "returns with 200 OK", %{conn: conn} do
assert {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)

assert {:ok, _conn, [{:status, ^ref, 200}, {:status_reason, ^ref, "OK"}]} =
HTTP1.stream(conn, {:tcp, conn.socket, "HTTP/1.1 200 OK\r\n"})
end

test "returns with 404 Not Found", %{conn: conn} do
assert {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)

assert {:ok, _conn, [{:status, ^ref, 404}, {:status_reason, ^ref, "Not Found"}]} =
HTTP1.stream(conn, {:tcp, conn.socket, "HTTP/1.1 404 Not Found\r\n"})
end

test "returns empty string when reason is not provided", %{conn: conn} do
assert {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)

assert {:ok, _conn, [{:status, ^ref, 200}, {:status_reason, ^ref, ""}]} =
HTTP1.stream(
conn,
{:tcp, conn.socket, "HTTP/1.1 200\r\n"}
)
end
end

describe "non-streaming requests" do
test "content-length header is added if not present",
%{conn: conn, server_socket: server_socket, port: port} do
Expand Down
2 changes: 1 addition & 1 deletion test/mint/http2/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ defmodule HTTP2.IntegrationTest do
assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn)

assert [{:status, ^ref, status}, {:headers, ^ref, headers} | rest] = responses
assert status in [200, 302]
assert status in [200, 301, 302]

assert {_, [{:done, ^ref}]} = Enum.split_while(rest, &match?({:data, ^ref, _}, &1))

Expand Down