diff --git a/lib/mint/http2.ex b/lib/mint/http2.ex
index 5c84e8d8..8a2cd51d 100644
--- a/lib/mint/http2.ex
+++ b/lib/mint/http2.ex
@@ -2080,6 +2080,13 @@ defmodule Mint.HTTP2 do
# https://httpwg.org/specs/rfc9113.html#HttpFraming
[{":status", <1, _, _>> = status} | headers] ->
cond do
+ # RFC 9113 8.6: HTTP/2 does not support the 101 status code.
+ status == "101" ->
+ conn = close_stream!(conn, stream.id, :protocol_error)
+ debug_data = "the 101 (Switching Protocols) status code is not supported in HTTP/2"
+ error = wrap_error({:protocol_error, debug_data})
+ {conn, [{:error, stream.ref, error} | responses]}
+
end_stream? ->
conn = close_stream!(conn, stream.id, :protocol_error)
debug_data = "informational response (1xx) must not have the END_STREAM flag set"
@@ -2217,14 +2224,32 @@ defmodule Mint.HTTP2 do
cond do
not valid_field_name?(name) -> {:error, {:invalid_header_name, name}}
not valid_field_value?(value) -> {:error, {:invalid_header_value, name, value}}
+ connection_specific?(name) -> {:error, connection_specific_error(name)}
true -> validate_response_headers(rest, trailers?, status?, true)
end
end
- # RFC 9110 15: status-code = 3DIGIT
- defp valid_status?(<>) when a in ?0..?9 and b in ?0..?9 and c in ?0..?9, do: true
+ # RFC 9110 15: status-code = 3DIGIT, with values in the range 100-999.
+ defp valid_status?(<>) when a in ?1..?9 and b in ?0..?9 and c in ?0..?9, do: true
defp valid_status?(_other), do: false
+ # RFC 9113 8.2.2: a message with connection-specific header fields is malformed.
+ # "te" is only allowed in requests, with the "trailers" value.
+ @connection_specific_headers [
+ "connection",
+ "keep-alive",
+ "proxy-connection",
+ "te",
+ "transfer-encoding",
+ "upgrade"
+ ]
+
+ defp connection_specific?(name), do: name in @connection_specific_headers
+
+ defp connection_specific_error(name) do
+ {:protocol_error, "connection-specific header #{inspect(name)} is not allowed in HTTP/2"}
+ end
+
# RFC 9113 8.2.1: a field name must not contain characters in 0x00-0x20, 0x41-0x5A
# (uppercase letters) or 0x7F-0xFF, and only a pseudo-header field can contain a colon.
defp valid_field_name?(<<>>), do: false
@@ -2333,14 +2358,11 @@ defmodule Mint.HTTP2 do
# If we receive RST_STREAM then the stream is definitely closed.
# We won't send anything else on the stream so we can simply delete
# it, so that if we get things like DATA on that stream we error out.
+ # Streams are removed as soon as the server ends them, so a RST_STREAM on a
+ # stream we still track means the response is incomplete, whatever the code.
conn = delete_stream(conn, stream)
-
- if error_code == :no_error do
- {conn, [{:done, stream.ref} | responses]}
- else
- error = wrap_error({:server_closed_request, error_code})
- {conn, [{:error, stream.ref, error} | responses]}
- end
+ error = wrap_error({:server_closed_request, error_code})
+ {conn, [{:error, stream.ref, error} | responses]}
:error ->
{conn, responses}
diff --git a/test/mint/http2/conn_test.exs b/test/mint/http2/conn_test.exs
index 8b4310b2..a2534f36 100644
--- a/test/mint/http2/conn_test.exs
+++ b/test/mint/http2/conn_test.exs
@@ -359,6 +359,36 @@ defmodule Mint.HTTP2Test do
end
describe "closed streams" do
+ for phase <- [:before_the_headers, :during_the_body] do
+ test "RST_STREAM with NO_ERROR #{phase} is an error", %{conn: conn} do
+ {conn, ref} = open_request(conn)
+
+ assert_recv_frames [headers(stream_id: stream_id)]
+
+ conn =
+ if unquote(phase) == :during_the_body do
+ assert {:ok, %HTTP2{} = conn,
+ [{:status, ^ref, 200}, {:headers, ^ref, _}, {:data, ^ref, "x"}]} =
+ stream_frames(conn, [
+ {:headers, stream_id, [{":status", "200"}, {"content-length", "5"}],
+ [:end_headers]},
+ data(stream_id: stream_id, data: "x")
+ ])
+
+ conn
+ else
+ conn
+ end
+
+ assert {:ok, %HTTP2{} = conn, [{:error, ^ref, error}]} =
+ stream_frames(conn, [rst_stream(stream_id: stream_id, error_code: :no_error)])
+
+ assert_http2_error error, {:server_closed_request, :no_error}
+ refute Map.has_key?(conn.streams, stream_id)
+ assert HTTP2.open?(conn)
+ end
+ end
+
test "server closes a stream with RST_STREAM", %{conn: conn} do
{conn, ref} = open_request(conn)
@@ -1231,7 +1261,7 @@ defmodule Mint.HTTP2Test do
end
describe "response header validation" do
- for status <- ["abc", "", "+200", "2000", "20", "200 ", " 200", "1ab"] do
+ for status <- ["abc", "", "+200", "2000", "20", "200 ", " 200", "1ab", "000", "099"] do
test "an invalid :status of #{inspect(status)} is a stream error", %{conn: conn} do
{conn, ref} = open_request(conn)
@@ -1250,6 +1280,89 @@ defmodule Mint.HTTP2Test do
end
end
+ for {name, value} <- [
+ {"connection", "close"},
+ {"keep-alive", "timeout=5"},
+ {"proxy-connection", "keep-alive"},
+ {"transfer-encoding", "chunked"},
+ {"upgrade", "websocket"},
+ {"te", "gzip"},
+ {"te", "trailers"}
+ ] do
+ test "the connection-specific header #{name}: #{value} is a stream error", %{conn: conn} do
+ {conn, ref} = open_request(conn)
+
+ assert_recv_frames [headers(stream_id: stream_id)]
+
+ assert {:ok, %HTTP2{} = conn, responses} =
+ stream_frames(conn, [
+ {:headers, stream_id, [{":status", "200"}, {unquote(name), unquote(value)}],
+ [:end_headers]}
+ ])
+
+ assert [{:error, ^ref, error}] = responses
+ assert_http2_error error, {:protocol_error, debug_data}
+ assert debug_data =~ "connection-specific header #{inspect(unquote(name))}"
+
+ assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
+ assert HTTP2.open?(conn)
+ end
+ end
+
+ test "a connection-specific header in an informational response is a stream error",
+ %{conn: conn} do
+ {conn, ref} = open_request(conn)
+
+ assert_recv_frames [headers(stream_id: stream_id)]
+
+ assert {:ok, %HTTP2{} = conn, responses} =
+ stream_frames(conn, [
+ {:headers, stream_id, [{":status", "103"}, {"te", "trailers"}], [:end_headers]}
+ ])
+
+ assert [{:error, ^ref, error}] = responses
+ assert_http2_error error, {:protocol_error, debug_data}
+ assert debug_data =~ "connection-specific header \"te\""
+
+ assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
+ assert HTTP2.open?(conn)
+ end
+
+ test "a connection-specific header in trailers is a stream error", %{conn: conn} do
+ {conn, ref} = open_request(conn)
+
+ assert_recv_frames [headers(stream_id: stream_id)]
+
+ assert {:ok, %HTTP2{} = conn, responses} =
+ stream_frames(conn, [
+ {:headers, stream_id, [{":status", "200"}], [:end_headers]},
+ {:headers, stream_id, [{"te", "trailers"}], [:end_headers, :end_stream]}
+ ])
+
+ assert [{:status, ^ref, 200}, {:headers, ^ref, []}, {:error, ^ref, error}] = responses
+ assert_http2_error error, {:protocol_error, debug_data}
+ assert debug_data =~ "connection-specific header \"te\""
+
+ assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
+ assert HTTP2.open?(conn)
+ end
+
+ test "a 101 status is a stream error", %{conn: conn} do
+ {conn, ref} = open_request(conn)
+
+ assert_recv_frames [headers(stream_id: stream_id)]
+
+ assert {:ok, %HTTP2{} = conn, responses} =
+ stream_frames(conn, [{:headers, stream_id, [{":status", "101"}], [:end_headers]}])
+
+ assert [{:error, ^ref, error}] = responses
+ assert_http2_error error, {:protocol_error, debug_data}
+ assert debug_data =~ "the 101 (Switching Protocols) status code is not supported in HTTP/2"
+
+ assert_recv_frames [rst_stream(stream_id: ^stream_id, error_code: :protocol_error)]
+ assert HTTP2.open?(conn)
+ end
+
for name <- ["Foo", "fo o", "", "foo:bar", "f\x7Fo", "f\xC3\xA4"] do
test "an invalid header name #{inspect(name)} is a stream error", %{conn: conn} do
{conn, ref} = open_request(conn)
@@ -1694,7 +1807,7 @@ defmodule Mint.HTTP2Test do
info_hbf =
server_encode_headers([
- {":status", "101"},
+ {":status", "102"},
{"x-info-header1", "this is an info"}
])