diff --git a/lib/mint/http2.ex b/lib/mint/http2.ex index 5c84e8d8..f48cd556 100644 --- a/lib/mint/http2.ex +++ b/lib/mint/http2.ex @@ -191,6 +191,9 @@ defmodule Mint.HTTP2 do # Fields of the connection. buffer: "", + # Highest stream ID the server has promised through PUSH_PROMISE. Promised IDs must + # increase, and it is the last server-initiated stream reported in GOAWAY frames. + last_promised_stream_id: 0, # `send_window_size` is the client *send* window for the connection # — how much request-body data we're allowed to send to the server # before it refills the window with a WINDOW_UPDATE frame. @@ -348,7 +351,9 @@ defmodule Mint.HTTP2 do * `:too_many_concurrent_requests` - when the maximum number of concurrent requests allowed by the server is reached. To find out what this limit is, use `get_setting/2` - with the `:max_concurrent_streams` setting name. + with the `:max_concurrent_streams` setting name. It's also returned for a promised + request whose pushed response is refused because it would exceed the client's + `:max_concurrent_streams` setting. * `{:max_header_list_size_exceeded, size, max_size}` - when the maximum size of the header list is reached. `size` is the actual value of the header list size, @@ -390,8 +395,9 @@ defmodule Mint.HTTP2 do When this error is returned, it means that the server hasn't processed the request at all, so it's safe to retry the given request on a different or new connection. - * `{:server_closed_request, error_code}` - when the server closes the request. - `error_code` is the reason why the request was closed. + * `{:server_closed_request, error_code}` - when the server closes the request before + the response is complete. `error_code` is the reason why the request was closed, + which can be `:no_error` when the server ends a response early. * `{:server_closed_connection, reason, debug_data}` - when the server closes the connection gracefully or because of an error. In HTTP/2, this corresponds to a `GOAWAY` frame. @@ -1409,6 +1415,13 @@ defmodule Mint.HTTP2 do end defp encode_stream_body_request_payload(conn, stream_id, {:eof, trailers}) do + stream = fetch_stream!(conn, stream_id) + + if stream.state != :open do + error = wrap_error(:request_is_not_streaming) + throw({:mint, conn, error}) + end + trailers = Headers.from_raw(trailers) if unallowed_trailer_header = Headers.find_unallowed_trailer(trailers) do @@ -1417,7 +1430,10 @@ defmodule Mint.HTTP2 do end trailer_headers = Headers.to_raw(trailers, _case_sensitive = false) - encode_headers(conn, stream_id, trailer_headers, [:end_headers, :end_stream]) + enabled_flags = [:end_headers, :end_stream] + {conn, payload} = encode_header_block(conn, stream_id, trailer_headers, enabled_flags) + conn = put_in(conn.streams[stream_id].state, :half_closed_local) + {conn, payload} end defp encode_stream_body_request_payload(conn, stream_id, iodata) do @@ -1439,12 +1455,7 @@ defmodule Mint.HTTP2 do end defp encode_headers(conn, stream_id, headers, enabled_flags) do - assert_headers_smaller_than_max_header_list_size(conn, headers) - - headers = Enum.map(headers, fn {name, value} -> {:store_name, name, value} end) - {hbf, conn} = get_and_update_in(conn.encode_table, &HPAX.encode(headers, &1)) - - payload = headers_to_encoded_frames(conn, stream_id, hbf, enabled_flags) + {conn, payload} = encode_header_block(conn, stream_id, headers, enabled_flags) stream_state = if :end_stream in enabled_flags, do: :half_closed_local, else: :open @@ -1454,6 +1465,15 @@ defmodule Mint.HTTP2 do {conn, payload} end + defp encode_header_block(conn, stream_id, headers, enabled_flags) do + assert_headers_smaller_than_max_header_list_size(conn, headers) + + headers = Enum.map(headers, fn {name, value} -> {:store_name, name, value} end) + {hbf, conn} = get_and_update_in(conn.encode_table, &HPAX.encode(headers, &1)) + + {conn, headers_to_encoded_frames(conn, stream_id, hbf, enabled_flags)} + end + defp assert_headers_smaller_than_max_header_list_size( %{server_settings: %{max_header_list_size: :infinity}}, _headers @@ -1855,13 +1875,22 @@ defmodule Mint.HTTP2 do :ok end - # RFC 9113 5.1: PRIORITY is the only frame the server can send on an idle stream. - # Client streams are opened in order, so odd stream IDs from next_stream_id on are - # idle. + # RFC 9113 5.1: PRIORITY frames are allowed on idle streams. Client streams are + # opened in order, so odd stream IDs from next_stream_id on are idle and the server + # can't send other frames on them. Server streams are only opened through + # PUSH_PROMISE (RFC 9113 8.4 and 5.1.1), so even stream IDs above the last promised + # one are idle too. defp assert_stream_id_is_allowed(_conn, :priority, _stream_id), do: :ok defp assert_stream_id_is_allowed(conn, _frame, stream_id) do - if Integer.is_odd(stream_id) and stream_id >= conn.next_stream_id do + idle? = + cond do + stream_id == 0 -> false + Integer.is_odd(stream_id) -> stream_id >= conn.next_stream_id + true -> stream_id > conn.last_promised_stream_id + end + + if idle? do debug_data = "frame with stream ID #{inspect(stream_id)} has not been opened yet" send_connection_error!(conn, :protocol_error, debug_data) else @@ -2080,6 +2109,13 @@ defmodule Mint.HTTP2 do # https://httpwg.org/specs/rfc9113.html#HttpFraming [{":status", <> = 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" @@ -2088,53 +2124,42 @@ defmodule Mint.HTTP2 do {conn, responses} true -> - assert_stream_in_state(conn, stream, [:open, :half_closed_local]) - status = String.to_integer(status) - headers = join_cookie_headers(headers) - new_responses = [{:headers, ref, headers}, {:status, ref, status} | responses] - {conn, new_responses} + case open_promised_stream(conn, stream) do + {:ok, conn} -> + status = String.to_integer(status) + headers = join_cookie_headers(headers) + new_responses = [{:headers, ref, headers}, {:status, ref, status} | responses] + {conn, new_responses} + + {:refused, conn} -> + error = wrap_error(:too_many_concurrent_requests) + {conn, [{:error, ref, error} | responses]} + end end [{":status", status} | headers] when not received_first_headers? -> status = String.to_integer(status) headers = join_cookie_headers(headers) - case response_content_length(stream, status, headers) do - {:ok, content_length} -> - conn = - update_in( - conn.streams[stream.id], - &%{&1 | received_first_headers?: true, content_length: content_length} - ) - - new_responses = [{:headers, ref, headers}, {:status, ref, status} | responses] - - cond do - # :reserved_remote means that this was a promised stream. As soon as headers come, - # the stream goes in the :half_closed_local state (unless it's not allowed because - # of the client's max concurrent streams limit, or END_STREAM is set). - stream.state == :reserved_remote -> - cond do - conn.open_server_stream_count >= conn.client_settings.max_concurrent_streams -> - conn = close_stream!(conn, stream.id, :refused_stream) - {conn, responses} - - end_stream? -> - end_remote_stream(conn, stream, new_responses) - - true -> - conn = update_in(conn.open_server_stream_count, &(&1 + 1)) - conn = update_in(conn.reserved_server_stream_count, &(&1 - 1)) - conn = put_in(conn.streams[stream.id].state, :half_closed_local) - {conn, new_responses} - end - - end_stream? -> - end_remote_stream(conn, stream, new_responses) - - true -> - {conn, new_responses} - end + with {:ok, content_length} <- response_content_length(stream, status, headers), + {:ok, conn} <- open_promised_stream(conn, stream) do + conn = + update_in( + conn.streams[stream.id], + &%{&1 | received_first_headers?: true, content_length: content_length} + ) + + new_responses = [{:headers, ref, headers}, {:status, ref, status} | responses] + + if end_stream? do + end_remote_stream(conn, stream, new_responses) + else + {conn, new_responses} + end + else + {:refused, conn} -> + error = wrap_error(:too_many_concurrent_requests) + {conn, [{:error, ref, error} | responses]} {:error, reason} -> conn = close_stream!(conn, stream.id, :protocol_error) @@ -2167,6 +2192,21 @@ defmodule Mint.HTTP2 do end end + # RFC 9113 5.1: HEADERS frames move a stream reserved by a PUSH_PROMISE to the + # half-closed (local) state, where it counts against the client's concurrency + # limit. Streams that don't fit within the limit are refused. + defp open_promised_stream(conn, %{state: :reserved_remote} = stream) do + if conn.open_server_stream_count >= conn.client_settings.max_concurrent_streams do + {:refused, close_stream!(conn, stream.id, :refused_stream)} + else + conn = update_in(conn.open_server_stream_count, &(&1 + 1)) + conn = update_in(conn.reserved_server_stream_count, &(&1 - 1)) + {:ok, put_in(conn.streams[stream.id].state, :half_closed_local)} + end + end + + defp open_promised_stream(conn, _stream), do: {:ok, conn} + defp decode_hbf(conn, hbf) do case HPAX.decode(hbf, conn.decode_table) do {:ok, headers, decode_table} -> @@ -2217,14 +2257,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 +2391,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} @@ -2502,10 +2557,18 @@ defmodule Mint.HTTP2 do hbf: hbf ) = frame - assert_valid_promised_stream_id(conn, promised_stream_id) + assert_valid_push_promise_stream_ids(conn, stream_id, promised_stream_id) + conn = put_in(conn.last_promised_stream_id, promised_stream_id) - stream = fetch_stream!(conn, stream_id) - assert_stream_in_state(conn, stream, [:open, :half_closed_local]) + # RFC 9113 6.6: the stream may already be closed because the client reset it + # before the server processed the RST_STREAM, so a missing stream is not an + # error. The header block still has to be decoded to keep the HPACK table in + # sync and the promised stream has to be reset. + stream = Map.get(conn.streams, stream_id) + + if stream do + assert_stream_in_state(conn, stream, [:open, :half_closed_local]) + end if flag_set?(flags, :push_promise, :end_headers) do decode_push_promise_headers_and_add_response( @@ -2540,27 +2603,45 @@ defmodule Mint.HTTP2 do # with the HEADERS that would open them. server_stream_count = conn.open_server_stream_count + conn.reserved_server_stream_count - if server_stream_count >= conn.client_settings.max_concurrent_streams do - conn = refuse_promised_stream(conn, promised_stream_id) - {conn, responses} - else - promised_stream = %{ - id: promised_stream_id, - ref: make_ref(), - state: :reserved_remote, - send_window_size: conn.server_settings.initial_window_size, - receive_window_size: conn.client_settings.initial_window_size, - receive_window_remaining: conn.client_settings.initial_window_size, - received_first_headers?: false, - method: promised_method(headers), - content_length: nil, - body_size: 0 - } - - conn = put_in(conn.streams[promised_stream.id], promised_stream) - conn = update_in(conn.reserved_server_stream_count, &(&1 + 1)) - new_response = {:push_promise, stream.ref, promised_stream.ref, headers} - {conn, [new_response | responses]} + cond do + is_nil(stream) -> + log( + conn, + :debug, + "Received PUSH_PROMISE frame on closed stream, resetting the promised stream" + ) + + conn = reset_promised_stream(conn, promised_stream_id, :cancel) + {conn, responses} + + debug_data = promised_headers_error(headers) -> + log(conn, :debug, "Resetting promised stream #{promised_stream_id}: #{debug_data}") + conn = reset_promised_stream(conn, promised_stream_id, :protocol_error) + {conn, responses} + + server_stream_count >= conn.client_settings.max_concurrent_streams -> + conn = reset_promised_stream(conn, promised_stream_id, :refused_stream) + {conn, responses} + + true -> + promised_stream = %{ + id: promised_stream_id, + ref: make_ref(), + state: :reserved_remote, + send_window_size: conn.server_settings.initial_window_size, + receive_window_size: conn.client_settings.initial_window_size, + receive_window_remaining: conn.client_settings.initial_window_size, + received_first_headers?: false, + method: promised_method(headers), + content_length: nil, + body_size: 0 + } + + conn = put_in(conn.streams[promised_stream.id], promised_stream) + conn = put_in(conn.ref_to_stream_id[promised_stream.ref], promised_stream.id) + conn = update_in(conn.reserved_server_stream_count, &(&1 + 1)) + new_response = {:push_promise, stream.ref, promised_stream.ref, headers} + {conn, [new_response | responses]} end end @@ -2571,25 +2652,33 @@ defmodule Mint.HTTP2 do end end - defp refuse_promised_stream(conn, promised_stream_id) do + defp reset_promised_stream(conn, promised_stream_id, error_code) do if open?(conn) do - rst_stream_frame = rst_stream(stream_id: promised_stream_id, error_code: :refused_stream) + rst_stream_frame = rst_stream(stream_id: promised_stream_id, error_code: error_code) send!(conn, Frame.encode(rst_stream_frame)) else conn end end - defp assert_valid_promised_stream_id(conn, promised_stream_id) do + # RFC 9113 8.4: PUSH_PROMISE frames are only allowed on client-initiated streams. + # RFC 9113 5.1.1: server-initiated streams have even identifiers, 0 is reserved for + # the connection, and the identifier of a new stream must be greater than all the + # streams the server has already opened or reserved. + defp assert_valid_push_promise_stream_ids(conn, stream_id, promised_stream_id) do cond do - not is_integer(promised_stream_id) or Integer.is_odd(promised_stream_id) -> + Integer.is_even(stream_id) -> + debug_data = "PUSH_PROMISE frame on server-initiated stream #{stream_id}" + send_connection_error!(conn, :protocol_error, debug_data) + + promised_stream_id == 0 or Integer.is_odd(promised_stream_id) -> debug_data = "invalid promised stream ID: #{inspect(promised_stream_id)}" send_connection_error!(conn, :protocol_error, debug_data) - Map.has_key?(conn.streams, promised_stream_id) -> + promised_stream_id <= conn.last_promised_stream_id -> debug_data = - "stream with ID #{inspect(promised_stream_id)} already exists and can't be " <> - "reserved by the server" + "promised stream ID #{promised_stream_id} is not greater than the last " <> + "promised stream ID #{conn.last_promised_stream_id}" send_connection_error!(conn, :protocol_error, debug_data) @@ -2598,6 +2687,84 @@ defmodule Mint.HTTP2 do end end + @promised_pseudo_headers [":method", ":scheme", ":authority", ":path"] + + # RFC 9113 8.4.1: a promised request must be cacheable and safe and must not have + # content, and RFC 9113 8.4 and 8.3.1 require the :method, :scheme, :authority and + # :path pseudo-headers. Field names and values follow the same rules as response + # headers, except that "te" is allowed with the "trailers" value (RFC 9113 8.2.2). + defp promised_headers_error(headers) do + case validate_promised_fields(headers, _pseudo = %{}, _regular? = false) do + {:error, debug_data} -> + debug_data + + {:ok, pseudo} -> + cond do + not Map.has_key?(pseudo, ":method") -> + "missing :method pseudo-header in promised request" + + pseudo[":scheme"] in [nil, ""] -> + "missing or empty :scheme pseudo-header in promised request" + + pseudo[":authority"] in [nil, ""] -> + "missing or empty :authority pseudo-header in promised request" + + not String.starts_with?(pseudo[":path"] || "", "/") -> + "missing or invalid :path pseudo-header in promised request" + + pseudo[":method"] not in ["GET", "HEAD"] -> + "promised request method #{inspect(pseudo[":method"])} is not safe and cacheable" + + true -> + case content_length(headers) do + {:ok, content_length} when content_length in [nil, 0] -> nil + {:ok, _content_length} -> "promised request must not have content" + {:error, _reason} -> "invalid content-length header in promised request" + end + end + end + end + + defp validate_promised_fields([], pseudo, _regular?), do: {:ok, pseudo} + + defp validate_promised_fields([{":" <> _ = name, value} | rest], pseudo, regular?) do + cond do + regular? -> + {:error, "pseudo-header #{inspect(name)} must appear before regular header fields"} + + name not in @promised_pseudo_headers -> + {:error, "undefined pseudo-header #{inspect(name)} in promised request"} + + Map.has_key?(pseudo, name) -> + {:error, "the #{name} pseudo-header appears more than once"} + + not valid_field_value?(value) -> + {:error, "invalid value for pseudo-header #{inspect(name)}"} + + true -> + validate_promised_fields(rest, Map.put(pseudo, name, value), regular?) + end + end + + defp validate_promised_fields([{name, value} | rest], pseudo, _regular?) do + cond do + not valid_field_name?(name) -> + {:error, "invalid header name #{inspect(name)}"} + + not valid_field_value?(value) -> + {:error, "invalid value for header #{inspect(name)}"} + + name == "te" and String.downcase(value, :ascii) == "trailers" -> + validate_promised_fields(rest, pseudo, true) + + connection_specific?(name) -> + {:error, elem(connection_specific_error(name), 1)} + + true -> + validate_promised_fields(rest, pseudo, true) + end + end + # PING defp handle_ping(conn, Frame.ping() = frame, responses) do @@ -2639,9 +2806,12 @@ defmodule Mint.HTTP2 do # We gather all the unprocessed requests and form {:error, _, _} tuples for each one. # At the same time, we delete all the unprocessed requests from the stream set. + # RFC 9113 6.8: the last stream ID only covers streams initiated by the client, so + # server-initiated (even) streams are never unprocessed. {unprocessed_request_responses, conn} = Enum.flat_map_reduce(conn.streams, conn, fn - {stream_id, _stream}, conn_acc when stream_id <= last_stream_id -> + {stream_id, _stream}, conn_acc + when Integer.is_even(stream_id) or stream_id <= last_stream_id -> {[], conn_acc} {_stream_id, stream}, conn_acc -> @@ -2773,7 +2943,12 @@ defmodule Mint.HTTP2 do defp send_connection_error!(conn, error_code, debug_data) do frame = - goaway(stream_id: 0, last_stream_id: 2, error_code: error_code, debug_data: debug_data) + goaway( + stream_id: 0, + last_stream_id: conn.last_promised_stream_id, + error_code: error_code, + debug_data: debug_data + ) # Try to send the GOAWAY frame and close connection. # If the frame fails to send, we still want to set the close @@ -2887,9 +3062,10 @@ defmodule Mint.HTTP2 do end def format_error(:too_many_concurrent_requests) do - "the number of max concurrent HTTP/2 requests supported by the server has been reached. " <> - "Use Mint.HTTP2.get_server_setting/2 with the :max_concurrent_streams setting name " <> - "to find out the maximum number of concurrent requests supported by the server." + "the maximum number of concurrent HTTP/2 streams has been reached. For requests, use " <> + "Mint.HTTP2.get_server_setting/2 with the :max_concurrent_streams setting name to find " <> + "out the limit supported by the server. For pushed responses, the limit is the " <> + ":max_concurrent_streams client setting." end def format_error({:max_header_list_size_exceeded, size, max_size}) do diff --git a/test/mint/http2/conn_test.exs b/test/mint/http2/conn_test.exs index 8b4310b2..6a7d7f47 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"} ]) @@ -1830,7 +1943,7 @@ defmodule Mint.HTTP2Test do assert_recv_frames [headers(stream_id: stream_id)] - promised_hbf = server_encode_headers([{":method", "GET"}]) + promised_hbf = server_encode_headers(promised_headers()) hbf1 = server_encode_headers([{":status", "200"}]) hbf2 = server_encode_headers([{":status", "200"}]) trailer_hbf = server_encode_headers([{"x-trailer", "some value"}]) @@ -1862,7 +1975,7 @@ defmodule Mint.HTTP2Test do ]) assert [ - {:push_promise, ^ref, promised_ref, [{":method", "GET"}]}, + {:push_promise, ^ref, promised_ref, promised_headers}, {:status, ^ref, 200}, {:headers, ^ref, []}, {:done, ^ref}, @@ -1872,6 +1985,7 @@ defmodule Mint.HTTP2Test do {:done, promised_ref} ] = responses + assert promised_headers == promised_headers() assert HTTP2.open?(conn) end @@ -1949,6 +2063,406 @@ defmodule Mint.HTTP2Test do end describe "server pushes" do + test "interim responses on a promised stream", %{conn: conn} do + {conn, ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, ^ref, promised_ref, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert {:ok, %HTTP2{} = conn, responses} = + stream_frames(conn, [ + {:headers, 2, [{":status", "103"}, {"link", "; rel=preload"}], + [:end_headers]}, + {:headers, 2, [{":status", "200"}], [:end_headers, :end_stream]} + ]) + + assert [ + {:status, ^promised_ref, 103}, + {:headers, ^promised_ref, [{"link", "; rel=preload"}]}, + {:status, ^promised_ref, 200}, + {:headers, ^promised_ref, []}, + {:done, ^promised_ref} + ] = responses + + refute_receive {:ssl, _socket, _data}, 100 + assert HTTP2.open_request_count(conn) == 1 + assert HTTP2.open?(conn) + end + + test "cancelling a promised request resets the promised stream", %{conn: conn} do + {conn, ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, ^ref, promised_ref, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert {:ok, %HTTP2{} = conn} = HTTP2.cancel_request(conn, promised_ref) + assert_recv_frames [rst_stream(stream_id: 2, error_code: :cancel)] + + assert {:ok, %HTTP2{} = conn, []} = + stream_frames(conn, [ + {:headers, 2, [{":status", "200"}], [:end_headers, :end_stream]} + ]) + + assert HTTP2.open?(conn) + assert HTTP2.open_request_count(conn) == 1 + end + + test "a pushed response ending with its HEADERS frame sends no RST_STREAM", %{conn: conn} do + {conn, ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, responses} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ), + {:headers, 2, [{":status", "304"}], [:end_headers, :end_stream]} + ]) + + assert [ + {:push_promise, ^ref, promised_ref, _}, + {:status, promised_ref, 304}, + {:headers, promised_ref, []}, + {:done, promised_ref} + ] = responses + + refute_receive {:ssl, _socket, _data}, 100 + assert HTTP2.open_request_count(conn) == 1 + end + + for {variant, fields} <- [ + missing_scheme: [{":method", "GET"}, {":authority", "localhost"}, {":path", "/"}], + empty_scheme: [ + {":method", "GET"}, + {":scheme", ""}, + {":authority", "localhost"}, + {":path", "/"} + ], + missing_authority: [{":method", "GET"}, {":scheme", "https"}, {":path", "/"}], + empty_authority: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", ""}, + {":path", "/"} + ], + empty_path: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", ""} + ], + asterisk_path: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "*"} + ], + relative_path: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "style.css"} + ], + unsafe_method: [ + {":method", "POST"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"} + ], + duplicate_method: [ + {":method", "GET"}, + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"} + ], + uppercase_name: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"}, + {"Foo", "bar"} + ], + control_in_value: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"}, + {"foo", "a\nb"} + ], + connection_header: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"}, + {"connection", "keep-alive"} + ], + te_other_than_trailers: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"}, + {"te", "gzip"} + ], + content: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"}, + {"content-length", "1"} + ], + invalid_content_length: [ + {":method", "GET"}, + {":scheme", "https"}, + {":authority", "localhost"}, + {":path", "/"}, + {"content-length", "zero"} + ] + ] do + test "a PUSH_PROMISE with #{variant} in the promised request resets the promised stream", + %{conn: conn} do + {conn, _ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, []} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(unquote(fields)), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert_recv_frames [rst_stream(stream_id: 2, error_code: :protocol_error)] + refute Map.has_key?(conn.streams, 2) + assert HTTP2.open?(conn) + end + end + + for {variant, fields} <- [ + te_trailers: [{"te", "trailers"}], + mixed_case_te_trailers: [{"te", "Trailers"}], + zero_content_length: [{"content-length", "00"}] + ] do + test "a PUSH_PROMISE with #{variant} in the promised request is accepted", + %{conn: conn} do + {conn, ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + promised_headers = promised_headers() ++ unquote(fields) + + assert {:ok, %HTTP2{} = conn, [{:push_promise, ^ref, _promised_ref, ^promised_headers}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + refute_receive {:ssl, _socket, _data}, 100 + assert HTTP2.open?(conn) + end + end + + for {name, promised_stream_id} <- [ + {"zero", 0}, + {"odd", 3}, + {"not greater than the previous promised ID", 2} + ] do + test "a PUSH_PROMISE with a #{name} promised stream ID is a connection error", + %{conn: conn} do + {conn, _ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, _, _, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 4, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert {:error, %HTTP2{} = conn, error, []} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: unquote(promised_stream_id), + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert_http2_error error, {:protocol_error, debug_data} + assert debug_data =~ "promised stream ID" + assert_recv_frames [goaway(last_stream_id: 4, error_code: :protocol_error)] + refute HTTP2.open?(conn) + end + end + + test "a PUSH_PROMISE on a server-initiated stream is a connection error", %{conn: conn} do + {conn, _ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, _, promised_ref, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert {:ok, %HTTP2{} = conn, + [{:status, ^promised_ref, 200}, {:headers, ^promised_ref, []}]} = + stream_frames(conn, [{:headers, 2, [{":status", "200"}], [:end_headers]}]) + + assert {:error, %HTTP2{} = conn, error, []} = + stream_frames(conn, [ + push_promise( + stream_id: 2, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 4, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert_http2_error error, {:protocol_error, debug_data} + assert debug_data =~ "PUSH_PROMISE frame on server-initiated stream 2" + refute HTTP2.open?(conn) + end + + test "a pushed response in flight is not affected by a GOAWAY", %{conn: conn} do + {conn, ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, ^ref, promised_ref, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 4, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert {:ok, %HTTP2{} = conn, + [{:status, ^promised_ref, 200}, {:headers, ^promised_ref, []}]} = + stream_frames(conn, [{:headers, 4, [{":status", "200"}], [:end_headers]}]) + + assert {:ok, %HTTP2{} = conn, []} = + stream_frames(conn, [ + goaway(last_stream_id: stream_id, error_code: :no_error, debug_data: "") + ]) + + assert {:ok, %HTTP2{}, [{:data, ^promised_ref, "body"}, {:done, ^promised_ref}]} = + stream_frames(conn, [ + data(stream_id: 4, data: "body", flags: set_flags(:data, [:end_stream])) + ]) + end + + test "a PUSH_PROMISE on a stream the client cancelled resets the promised stream", + %{conn: conn} do + {conn, ref} = open_request(conn) + {:ok, conn} = HTTP2.cancel_request(conn, ref) + + assert_recv_frames [ + headers(stream_id: stream_id), + rst_stream(stream_id: stream_id, error_code: :cancel) + ] + + hbf = server_encode_headers([{":method", "GET"}, {"x-promised", "value"}]) + + assert {:ok, %HTTP2{} = conn, []} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: hbf, + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ), + ping(opaque_data: <<0::64>>) + ]) + + assert HTTP2.open?(conn) + + assert_recv_frames [ + rst_stream(stream_id: 2, error_code: :cancel), + ping(opaque_data: <<0::64>>) + ] + + # The header block was decoded, so the HPACK table is still in sync. + {conn, ref} = open_request(conn) + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{}, responses} = + stream_frames(conn, [ + {:headers, stream_id, [{":status", "200"}, {"x-promised", "value"}], + [:end_headers, :end_stream]} + ]) + + assert [{:status, ^ref, 200}, {:headers, ^ref, [{"x-promised", "value"}]}, {:done, ^ref}] = + responses + end + + test "a PUSH_PROMISE with CONTINUATIONs on a stream the client cancelled resets the promised stream", + %{conn: conn} do + {conn, ref} = open_request(conn) + {:ok, conn} = HTTP2.cancel_request(conn, ref) + + assert_recv_frames [ + headers(stream_id: stream_id), + rst_stream(stream_id: stream_id, error_code: :cancel) + ] + + <> = server_encode_headers([{":method", "GET"}, {"a", "b"}]) + + assert {:ok, %HTTP2{} = conn, []} = + stream_frames(conn, [ + push_promise(stream_id: stream_id, hbf: hbf1, promised_stream_id: 2), + continuation( + stream_id: stream_id, + hbf: hbf2, + flags: set_flags(:continuation, [:end_headers]) + ) + ]) + + assert HTTP2.open?(conn) + assert_recv_frames [rst_stream(stream_id: 2, error_code: :cancel)] + end + test "a PUSH_PROMISE frame and a few CONTINUATION frames are received", %{conn: conn} do promised_stream_id = 4 @@ -1958,7 +2472,7 @@ defmodule Mint.HTTP2Test do assert_recv_frames [headers(stream_id: stream_id)] # Promised headers. - headers = [{":method", "GET"}, {"foo", "bar"}, {"baz", "bong"}] + headers = promised_headers() ++ [{"foo", "bar"}, {"baz", "bong"}] <> = server_encode_headers(headers) @@ -1993,7 +2507,7 @@ defmodule Mint.HTTP2Test do ] = responses assert is_reference(promised_ref) - assert headers == [{":method", "GET"}, {"foo", "bar"}, {"baz", "bong"}] + assert headers == promised_headers() ++ [{"foo", "bar"}, {"baz", "bong"}] assert {:ok, %HTTP2{} = conn, responses} = stream_frames(conn, [ @@ -2026,7 +2540,7 @@ defmodule Mint.HTTP2Test do assert_recv_frames [headers(stream_id: stream_id)] - hbf = server_encode_headers([{":method", "GET"}]) + hbf = server_encode_headers(promised_headers()) assert {:error, %HTTP2{} = conn, error, []} = stream_frames(conn, [ @@ -2051,7 +2565,7 @@ defmodule Mint.HTTP2Test do assert_recv_frames [headers(stream_id: stream_id)] - promised_headers_hbf = server_encode_headers([{":method", "GET"}]) + promised_headers_hbf = server_encode_headers(promised_headers()) normal_headers_hbf = server_encode_headers([{":status", "200"}]) assert {:error, %HTTP2{} = conn, error, _responses} = @@ -2076,7 +2590,9 @@ defmodule Mint.HTTP2Test do ]) assert_http2_error error, {:protocol_error, debug_data} - assert debug_data =~ "stream with ID 4 already exists and can't be reserved by the server" + + assert debug_data =~ + "promised stream ID 4 is not greater than the last promised stream ID 4" refute HTTP2.open?(conn) end @@ -2088,7 +2604,7 @@ defmodule Mint.HTTP2Test do assert_recv_frames [headers(stream_id: stream_id)] - promised_headers_hbf = server_encode_headers([{":method", "GET"}]) + promised_headers_hbf = server_encode_headers(promised_headers()) normal_headers_hbf = server_encode_headers([{":status", "200"}]) assert {:ok, %HTTP2{} = conn, responses} = @@ -2130,6 +2646,40 @@ defmodule Mint.HTTP2Test do assert HTTP2.open?(conn) end + for {variant, status} <- [final: "200", interim: "103"] do + test "a promised stream refused at #{variant} HEADERS time returns an error", + %{conn: conn} do + {conn, ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, ^ref, promised_ref, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + {:ok, conn} = HTTP2.put_settings(conn, max_concurrent_streams: 0) + assert_recv_frames [settings()] + + assert {:ok, %HTTP2{} = conn, responses} = + stream_frames(conn, [ + settings(flags: set_flags(:settings, [:ack]), params: []), + {:headers, 2, [{":status", unquote(status)}], [:end_headers]} + ]) + + assert [{:error, ^promised_ref, error}] = responses + assert_http2_error error, :too_many_concurrent_requests + assert_recv_frames [rst_stream(stream_id: 2, error_code: :refused_stream)] + refute Map.has_key?(conn.streams, 2) + assert HTTP2.open?(conn) + end + end + @tag connect_options: [client_settings: [max_concurrent_streams: 5]] test "a flood of PUSH_PROMISE frames cannot grow the streams map past max_concurrent_streams", %{conn: conn} do @@ -2137,7 +2687,7 @@ defmodule Mint.HTTP2Test do assert_recv_frames [headers(stream_id: stream_id)] - promised_headers_hbf = server_encode_headers([{":method", "GET"}]) + promised_headers_hbf = server_encode_headers(promised_headers()) # The server promises many more streams than the client's limit but never # follows up with the response HEADERS for any of them. Each promise must @@ -2179,6 +2729,28 @@ defmodule Mint.HTTP2Test do end describe "misbehaving server" do + test "the GOAWAY sent on a connection error carries the last promised stream ID", + %{conn: conn} do + {conn, _ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, _, _, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 6, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + data = IO.iodata_to_binary(encode_raw(_ping = 0x06, 0x00, 3, <<0::64>>)) + assert {:error, %HTTP2{}, _error, []} = HTTP2.stream(conn, {:ssl, conn.socket, data}) + + assert_recv_frames [goaway(last_stream_id: 6, error_code: :protocol_error)] + end + test "an extension frame in the middle of a header block is a connection error", %{conn: conn} do {conn, _ref} = open_request(conn) @@ -2210,6 +2782,29 @@ defmodule Mint.HTTP2Test do refute HTTP2.open?(conn) end + for {frame_name, frame} <- [ + headers: quote(do: {:headers, 2, [{":status", "200"}], [:end_headers]}), + data: quote(do: data(stream_id: 2, data: "some data")), + rst_stream: quote(do: rst_stream(stream_id: 2, error_code: :cancel)), + window_update: quote(do: window_update(stream_id: 2, window_size_increment: 1)) + ] do + test "a #{frame_name} frame on a server stream that was never promised is a connection error", + %{conn: conn} do + {conn, _ref} = open_request(conn) + + assert_recv_frames [headers(stream_id: _stream_id)] + + assert {:error, %HTTP2{} = conn, error, []} = stream_frames(conn, [unquote(frame)]) + + assert_http2_error error, {:protocol_error, debug_data} + assert debug_data =~ "frame with stream ID 2 has not been opened yet" + + assert_recv_frames [goaway(error_code: :protocol_error)] + + refute HTTP2.open?(conn) + end + end + test "PRIORITY frames on idle streams are ignored", %{conn: conn} do {conn, _ref} = open_request(conn) @@ -3430,6 +4025,62 @@ defmodule Mint.HTTP2Test do assert server_decode_headers(trailer_hbf1 <> trailer_hbf2) == trailer_headers end + test "trailers keep the open request count", %{conn: conn} do + {conn, ref} = open_request(conn, :stream) + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn} = + HTTP2.stream_request_body(conn, ref, {:eof, [{"x-trailer", "value"}]}) + + assert_recv_frames [headers(stream_id: ^stream_id)] + assert HTTP2.open_request_count(conn) == 1 + + assert {:ok, %HTTP2{} = conn, [{:status, ^ref, 200}, {:headers, ^ref, []}, {:done, ^ref}]} = + stream_frames(conn, [ + {:headers, stream_id, [{":status", "200"}], [:end_headers, :end_stream]} + ]) + + assert HTTP2.open_request_count(conn) == 0 + end + + test "trailers on a request whose body has ended return an error", %{conn: conn} do + {conn, ref} = open_request(conn) + assert_recv_frames [headers(stream_id: _stream_id)] + + assert {:error, %HTTP2{} = conn, error} = + HTTP2.stream_request_body(conn, ref, {:eof, [{"x-trailer", "value"}]}) + + assert_http2_error error, :request_is_not_streaming + refute_receive {:ssl, _socket, _data}, 100 + assert HTTP2.open_request_count(conn) == 1 + end + + test "trailers on a promised request return an error", %{conn: conn} do + {conn, ref} = open_request(conn) + assert_recv_frames [headers(stream_id: stream_id)] + + assert {:ok, %HTTP2{} = conn, [{:push_promise, ^ref, promised_ref, _}]} = + stream_frames(conn, [ + push_promise( + stream_id: stream_id, + hbf: server_encode_headers(promised_headers()), + promised_stream_id: 2, + flags: set_flags(:push_promise, [:end_headers]) + ) + ]) + + assert {:error, %HTTP2{} = conn, error} = + HTTP2.stream_request_body(conn, promised_ref, {:eof, [{"x-trailer", "value"}]}) + + assert_http2_error error, :request_is_not_streaming + refute_receive {:ssl, _socket, _data}, 100 + + assert {conn.open_client_stream_count, conn.open_server_stream_count, + conn.reserved_server_stream_count} == {1, 0, 1} + + assert conn.streams[2].state == :reserved_remote + end + test "unallowed trailer headers cause an error", %{conn: conn} do {conn, ref} = open_request(conn, :stream) @@ -3903,6 +4554,10 @@ defmodule Mint.HTTP2Test do headers end + defp promised_headers do + [{":method", "GET"}, {":scheme", "https"}, {":authority", "localhost"}, {":path", "/"}] + end + defp open_request(conn, body \\ nil) do assert {:ok, %HTTP2{} = conn, ref} = HTTP2.request(conn, "GET", "/", [], body) assert is_reference(ref)