diff --git a/lib/mint/http.ex b/lib/mint/http.ex index e6a2abc4..5fceaf24 100644 --- a/lib/mint/http.ex +++ b/lib/mint/http.ex @@ -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 diff --git a/lib/mint/http1.ex b/lib/mint/http1.ex index 8c05e2cf..5b27de96 100644 --- a/lib/mint/http1.ex +++ b/lib/mint/http1.ex @@ -83,6 +83,8 @@ defmodule Mint.HTTP1 do """ @type error_reason() :: term() + @optional_responses_opts [:status_reason] + defstruct [ :host, :port, @@ -99,7 +101,8 @@ defmodule Mint.HTTP1 do buffer: "", proxy_headers: [], private: %{}, - log: false + log: false, + optional_responses: [] ] defmacrop log(conn, level, message) do @@ -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()) :: @@ -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} @@ -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`. """ @@ -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 -> @@ -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}} diff --git a/test/mint/http1/conn_test.exs b/test/mint/http1/conn_test.exs index b627c386..66f39876 100644 --- a/test/mint/http1/conn_test.exs +++ b/test/mint/http1/conn_test.exs @@ -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) @@ -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 diff --git a/test/mint/http2/integration_test.exs b/test/mint/http2/integration_test.exs index 3a901c63..1cfccdfb 100644 --- a/test/mint/http2/integration_test.exs +++ b/test/mint/http2/integration_test.exs @@ -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))