Skip to content

Commit 1ab3eeb

Browse files
authored
Introduce new HTTP/1 option for status reason-phrase (#471)
1 parent 8f95bc7 commit 1ab3eeb

File tree

4 files changed

+89
-5
lines changed

4 files changed

+89
-5
lines changed

lib/mint/http.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,12 @@ defmodule Mint.HTTP do
759759
You can have zero or more `1xx` `:status` and `:headers` responses for a
760760
single request, but they all precede a single non-`1xx` `:status` response.
761761
762+
* `{:status_reason, request_ref, reason_phrase}` - returned when the server replied
763+
with a response status code and a reason-phrase. The reason-phrase is a string.
764+
Returned when the `:optional_responses` option is passed to `connect/4`, with
765+
`:status_reason` in the list. See `Mint.HTTP1.connect/4` for more information.
766+
This is only available for HTTP/1.1 connections. *Available since v1.7.2*.
767+
762768
* `{:headers, request_ref, headers}` - returned when the server replied
763769
with a list of headers. Headers are in the form `{header_name, header_value}`
764770
with `header_name` and `header_value` being strings. A single `:headers` response

lib/mint/http1.ex

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ defmodule Mint.HTTP1 do
8383
"""
8484
@type error_reason() :: term()
8585

86+
@optional_responses_opts [:status_reason]
87+
8688
defstruct [
8789
:host,
8890
:port,
@@ -99,7 +101,8 @@ defmodule Mint.HTTP1 do
99101
buffer: "",
100102
proxy_headers: [],
101103
private: %{},
102-
log: false
104+
log: false,
105+
optional_responses: []
103106
]
104107

105108
defmacrop log(conn, level, message) do
@@ -128,6 +131,12 @@ defmodule Mint.HTTP1 do
128131
will not be validated. You might want this if you deal with non standard-
129132
conforming URIs but need to preserve them. The default is to validate the request
130133
target. *Available since v1.7.0*.
134+
* `:optional_responses` - (list of atoms) a list of optional responses to return.
135+
The possible values in the list are -
136+
* `:status_reason` which will return the
137+
[reason-phrase](https://datatracker.ietf.org/doc/html/rfc9112#name-status-line)
138+
for the status code, if it is returned by the server in status-line.
139+
This is only available for HTTP/1.1 connections. *Available since v1.7.2*.
131140
132141
"""
133142
@spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) ::
@@ -206,7 +215,8 @@ defmodule Mint.HTTP1 do
206215
state: :open,
207216
log: log?,
208217
case_sensitive_headers: Keyword.get(opts, :case_sensitive_headers, false),
209-
skip_target_validation: Keyword.get(opts, :skip_target_validation, false)
218+
skip_target_validation: Keyword.get(opts, :skip_target_validation, false),
219+
optional_responses: validate_optional_response_values(opts)
210220
}
211221

212222
{:ok, conn}
@@ -217,6 +227,21 @@ defmodule Mint.HTTP1 do
217227
end
218228
end
219229

230+
defp validate_optional_response_values(opts) do
231+
opts
232+
|> Keyword.get(:optional_responses, [])
233+
|> Enum.map(fn opt ->
234+
if opt not in @optional_responses_opts do
235+
raise ArgumentError, """
236+
invalid :optional_responses value #{opt}.
237+
allowed values are - #{inspect(@optional_responses_opts)}
238+
"""
239+
end
240+
241+
opt
242+
end)
243+
end
244+
220245
@doc """
221246
See `Mint.HTTP.close/1`.
222247
"""
@@ -646,10 +671,10 @@ defmodule Mint.HTTP1 do
646671

647672
defp decode(:status, %{request: request} = conn, data, responses) do
648673
case Response.decode_status_line(data) do
649-
{:ok, {version, status, _reason}, rest} ->
674+
{:ok, {version, status, _reason} = status_line, rest} ->
650675
request = %{request | version: version, status: status, state: :headers}
651676
conn = %{conn | request: request}
652-
responses = [{:status, request.ref, status} | responses]
677+
responses = put_status_responses(conn, status_line, responses)
653678
decode(:headers, conn, rest, responses)
654679

655680
:more ->
@@ -872,6 +897,20 @@ defmodule Mint.HTTP1 do
872897
end
873898
end
874899

900+
defp put_status_responses(
901+
%{request: request, optional_responses: optional_responses},
902+
{_version, status, reason},
903+
responses
904+
) do
905+
responses = [{:status, request.ref, status} | responses]
906+
907+
if Enum.member?(optional_responses, :status_reason) do
908+
[{:status_reason, request.ref, reason} | responses]
909+
else
910+
responses
911+
end
912+
end
913+
875914
defp store_header(%{content_length: nil} = request, "content-length", value) do
876915
with {:ok, content_length} <- Parse.content_length_header(value),
877916
do: {:ok, %{request | content_length: content_length}}

test/mint/http1/conn_test.exs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ defmodule Mint.HTTP1Test do
187187
refute HTTP1.open?(conn)
188188
end
189189

190+
test "raises error connecting with invalid optional_responses params", %{port: port} do
191+
assert_raise ArgumentError, fn ->
192+
HTTP1.connect(:http, "localhost", port, optional_responses: [:someting])
193+
end
194+
end
195+
190196
test "pipeline", %{conn: conn} do
191197
{:ok, conn, ref1} = HTTP1.request(conn, "GET", "/", [], nil)
192198
{:ok, conn, ref2} = HTTP1.request(conn, "GET", "/", [], nil)
@@ -581,6 +587,39 @@ defmodule Mint.HTTP1Test do
581587
end
582588
end
583589

590+
describe "status reason" do
591+
setup %{port: port} do
592+
assert {:ok, conn} =
593+
HTTP1.connect(:http, "localhost", port, optional_responses: [:status_reason])
594+
595+
[conn: conn]
596+
end
597+
598+
test "returns with 200 OK", %{conn: conn} do
599+
assert {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)
600+
601+
assert {:ok, _conn, [{:status, ^ref, 200}, {:status_reason, ^ref, "OK"}]} =
602+
HTTP1.stream(conn, {:tcp, conn.socket, "HTTP/1.1 200 OK\r\n"})
603+
end
604+
605+
test "returns with 404 Not Found", %{conn: conn} do
606+
assert {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)
607+
608+
assert {:ok, _conn, [{:status, ^ref, 404}, {:status_reason, ^ref, "Not Found"}]} =
609+
HTTP1.stream(conn, {:tcp, conn.socket, "HTTP/1.1 404 Not Found\r\n"})
610+
end
611+
612+
test "returns empty string when reason is not provided", %{conn: conn} do
613+
assert {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)
614+
615+
assert {:ok, _conn, [{:status, ^ref, 200}, {:status_reason, ^ref, ""}]} =
616+
HTTP1.stream(
617+
conn,
618+
{:tcp, conn.socket, "HTTP/1.1 200\r\n"}
619+
)
620+
end
621+
end
622+
584623
describe "non-streaming requests" do
585624
test "content-length header is added if not present",
586625
%{conn: conn, server_socket: server_socket, port: port} do

test/mint/http2/integration_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ defmodule HTTP2.IntegrationTest do
124124
assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn)
125125

126126
assert [{:status, ^ref, status}, {:headers, ^ref, headers} | rest] = responses
127-
assert status in [200, 302]
127+
assert status in [200, 301, 302]
128128

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

0 commit comments

Comments
 (0)