Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
29 changes: 25 additions & 4 deletions lib/mint/http1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,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 +129,11 @@ 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 +212,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: Keyword.get(opts, :optional_responses, [])
}

{:ok, conn}
Expand Down Expand Up @@ -646,10 +653,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 +879,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
33 changes: 33 additions & 0 deletions test/mint/http1/conn_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,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
Loading