From 48a00663c2781bb26d8fb09971e23d616df873d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Sat, 19 Sep 2026 19:29:35 +0200 Subject: [PATCH] Keep the HTTP/2 receive window credit in sync when the window shrinks Shrinking :initial_window_size adjusted each stream's receive window size but not its remaining credit. The server applies the difference to its view of the window as well (RFC 9113 6.9.2), so the next refill computed an increment of zero or a negative number and sent it in a WINDOW_UPDATE frame. A negative increment lands in the unsigned 31-bit field as a value near 2^31, which the server has to answer with a FLOW_CONTROL_ERROR stream reset (RFC 9113 6.9.1), and a zero increment is a PROTOCOL_ERROR stream error (RFC 9113 6.9). The remaining credit now follows the size change, and a refill is only sent when the increment is positive. --- lib/mint/http2.ex | 19 ++++++++++++------- test/mint/http2/conn_test.exs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/lib/mint/http2.ex b/lib/mint/http2.ex index 21527787..c81c77dc 100644 --- a/lib/mint/http2.ex +++ b/lib/mint/http2.ex @@ -1911,8 +1911,9 @@ defmodule Mint.HTTP2 do end defp maybe_refill_conn(frames, conn) do - if conn.receive_window_remaining <= conn.receive_window_update_threshold do - increment = conn.receive_window_size - conn.receive_window_remaining + increment = conn.receive_window_size - conn.receive_window_remaining + + if conn.receive_window_remaining <= conn.receive_window_update_threshold and increment > 0 do [window_update(stream_id: 0, window_size_increment: increment) | frames] else frames @@ -1922,9 +1923,10 @@ defmodule Mint.HTTP2 do defp maybe_refill_stream(frames, conn, stream_id) do case Map.fetch(conn.streams, stream_id) do {:ok, stream} -> - if stream.receive_window_remaining <= conn.receive_window_update_threshold do - increment = stream.receive_window_size - stream.receive_window_remaining + increment = stream.receive_window_size - stream.receive_window_remaining + if stream.receive_window_remaining <= conn.receive_window_update_threshold and + increment > 0 do [ window_update(stream_id: stream_id, window_size_increment: increment) | frames ] @@ -2255,9 +2257,12 @@ defmodule Mint.HTTP2 do for {stream_id, stream} <- streams, stream.state in [:open, :half_closed_local, :reserved_remote], into: streams do - receive_window_size = stream.receive_window_size + diff - - {stream_id, %{stream | receive_window_size: receive_window_size}} + {stream_id, + %{ + stream + | receive_window_size: stream.receive_window_size + diff, + receive_window_remaining: stream.receive_window_remaining + diff + }} end end) diff --git a/test/mint/http2/conn_test.exs b/test/mint/http2/conn_test.exs index b323be46..78753e56 100644 --- a/test/mint/http2/conn_test.exs +++ b/test/mint/http2/conn_test.exs @@ -2247,6 +2247,40 @@ defmodule Mint.HTTP2Test do end describe "settings" do + @tag connect_options: [ + receive_window_update_threshold: 8, + client_settings: [initial_window_size: 16] + ] + test "shrinking the initial window size keeps the remaining credit in sync", + %{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]}, + data(stream_id: stream_id, data: "123456") + ]) + + assert conn.streams[stream_id].receive_window_remaining == 10 + + {:ok, conn} = HTTP2.put_settings(conn, initial_window_size: 8) + assert_recv_frames [settings(params: [initial_window_size: 8])] + + assert {:ok, %HTTP2{} = conn, []} = + stream_frames(conn, [settings(flags: set_flags(:settings, [:ack]), params: [])]) + + assert conn.streams[stream_id].receive_window_size == 8 + assert conn.streams[stream_id].receive_window_remaining == 2 + + assert {:ok, %HTTP2{} = conn, [{:data, ^ref, "12"}]} = + stream_frames(conn, [data(stream_id: stream_id, data: "12")]) + + assert conn.streams[stream_id].receive_window_remaining == 8 + assert_recv_frames [window_update(stream_id: ^stream_id, window_size_increment: 8)] + end + test "put_settings/2 can be used to send settings to server", %{conn: conn} do {:ok, conn} = HTTP2.put_settings(conn, max_concurrent_streams: 123, initial_window_size: 1_000)