Skip to content

Commit 565ffc3

Browse files
committed
Track advertised receive window synchronously in initiate/5
Streams opened before the server's SETTINGS ACK arrived were reading their initial receive window from `conn.client_settings`, which still held library defaults at that point. If the user advertised a stream window smaller than the default (e.g. `initial_window_size: 65_535`), the stream struct tracked the 4 MB default locally while the server respected the 65_535 we sent in SETTINGS. The client's remaining window never dropped to the refill threshold, stream-level WINDOW_UPDATE frames never fired, and the connection stalled once the server exhausted its per-stream send window. Mirror the advertised `client_settings_params` into `conn.client_settings` during `initiate/5` — the sender already knows what it committed to and doesn't need to wait for the ACK to act on it. Add a regression test that opens a stream before the ACK round trip and asserts the stream struct reflects the advertised value. Also rename `receive_window` to `receive_window_remaining` so the peak/remaining distinction is clear at the call site, and document that `:receive_window_update_threshold` is shared between the connection and per-stream windows (so windows at or below the threshold refill on every DATA frame).
1 parent fa37715 commit 565ffc3

3 files changed

Lines changed: 86 additions & 30 deletions

File tree

lib/mint/http.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ defmodule Mint.HTTP do
247247
window that must remain on a connection or stream before a `WINDOW_UPDATE`
248248
frame is sent to refill it. Lower values send more frequent, smaller updates;
249249
higher values batch updates into fewer, larger ones. Defaults to 160_000
250-
(approximately 10× the default max frame size).
250+
(approximately 10× the default max frame size). The same threshold applies
251+
to both the connection and per-stream windows; when a window's peak size is
252+
at or below the threshold, the client refills after every DATA frame on
253+
that window.
251254
252255
There may be further protocol specific options that only take effect when the corresponding
253256
connection is established. Check `Mint.HTTP1.connect/4` and `Mint.HTTP2.connect/4` for

lib/mint/http2.ex

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,12 @@ defmodule Mint.HTTP2 do
195195
# matching WINDOW_UPDATE to bring the server's view from the spec
196196
# default of 65_535 up to the advertised peak.
197197
receive_window_size: @default_window_size,
198-
# `receive_window` is the server's current view of our receive
198+
# `receive_window_remaining` is the server's current view of our receive
199199
# window — decremented by DATA frame sizes as they arrive, bumped
200200
# back up to `receive_window_size` whenever we send a
201201
# WINDOW_UPDATE. When it drops to `receive_window_update_threshold`, we
202202
# refill it back to the peak in one frame.
203-
receive_window: @default_window_size,
203+
receive_window_remaining: @default_window_size,
204204
# Minimum remaining receive window before we send a WINDOW_UPDATE.
205205
# Configurable via the `:receive_window_update_threshold` connect option.
206206
receive_window_update_threshold: @default_receive_window_update_threshold,
@@ -912,7 +912,7 @@ defmodule Mint.HTTP2 do
912912
def set_window_size(%__MODULE__{} = conn, :connection, new_size) do
913913
do_set_window_size(conn, 0, conn.receive_window_size, new_size, fn conn, size ->
914914
conn = put_in(conn.receive_window_size, size)
915-
put_in(conn.receive_window, size)
915+
put_in(conn.receive_window_remaining, size)
916916
end)
917917
catch
918918
:throw, {:mint, conn, error} -> {:error, conn, error}
@@ -925,7 +925,7 @@ defmodule Mint.HTTP2 do
925925

926926
do_set_window_size(conn, stream_id, current, new_size, fn conn, size ->
927927
conn = put_in(conn.streams[stream_id].receive_window_size, size)
928-
put_in(conn.streams[stream_id].receive_window, size)
928+
put_in(conn.streams[stream_id].receive_window_remaining, size)
929929
end)
930930

931931
:error ->
@@ -935,7 +935,8 @@ defmodule Mint.HTTP2 do
935935
:throw, {:mint, conn, error} -> {:error, conn, error}
936936
end
937937

938-
defp do_set_window_size(conn, _stream_id, current, new_size, _update) when new_size == current do
938+
defp do_set_window_size(conn, _stream_id, current, new_size, _update)
939+
when new_size == current do
939940
{:ok, conn}
940941
end
941942

@@ -1121,9 +1122,19 @@ defmodule Mint.HTTP2 do
11211122
scheme_string = Atom.to_string(scheme)
11221123
mode = Keyword.get(opts, :mode, :active)
11231124
log? = Keyword.get(opts, :log, false)
1124-
connection_window_size = Keyword.get(opts, :connection_window_size, @default_connection_window_size)
1125+
1126+
connection_window_size =
1127+
Keyword.get(opts, :connection_window_size, @default_connection_window_size)
1128+
11251129
validate_window_size!(:connection_window_size, connection_window_size)
1126-
receive_window_update_threshold = Keyword.get(opts, :receive_window_update_threshold, @default_receive_window_update_threshold)
1130+
1131+
receive_window_update_threshold =
1132+
Keyword.get(
1133+
opts,
1134+
:receive_window_update_threshold,
1135+
@default_receive_window_update_threshold
1136+
)
1137+
11271138
validate_receive_window_update_threshold!(receive_window_update_threshold)
11281139
client_settings_params = Keyword.get(opts, :client_settings, [])
11291140

@@ -1160,10 +1171,21 @@ defmodule Mint.HTTP2 do
11601171
state: :handshaking,
11611172
log: log?,
11621173
receive_window_size: connection_window_size,
1163-
receive_window: connection_window_size,
1174+
receive_window_remaining: connection_window_size,
11641175
receive_window_update_threshold: receive_window_update_threshold
11651176
}
11661177

1178+
# Mirror the advertised client settings into `conn.client_settings` up
1179+
# front. Streams opened before the server's SETTINGS ACK arrives read
1180+
# their initial receive window from this map; without this, they would
1181+
# track the library default instead of the value we actually sent in
1182+
# the SETTINGS frame, and stream-level WINDOW_UPDATEs would never fire
1183+
# when the advertised window is smaller than the default.
1184+
conn =
1185+
update_in(conn.client_settings, fn settings ->
1186+
Enum.into(client_settings_params, settings)
1187+
end)
1188+
11671189
preface = build_preface(client_settings_params, connection_window_size)
11681190

11691191
with :ok <- Util.inet_opts(transport, socket),
@@ -1286,7 +1308,7 @@ defmodule Mint.HTTP2 do
12861308
receive_window_size: conn.client_settings.initial_window_size,
12871309
# Current remaining receive window for this stream, tracked
12881310
# independently from the peak so that refills can be batched.
1289-
receive_window: conn.client_settings.initial_window_size,
1311+
receive_window_remaining: conn.client_settings.initial_window_size,
12901312
received_first_headers?: false
12911313
}
12921314

@@ -1417,10 +1439,14 @@ defmodule Mint.HTTP2 do
14171439

14181440
cond do
14191441
data_size > stream.send_window_size ->
1420-
throw({:mint, conn, wrap_error({:exceeds_window_size, :request, stream.send_window_size})})
1442+
throw(
1443+
{:mint, conn, wrap_error({:exceeds_window_size, :request, stream.send_window_size})}
1444+
)
14211445

14221446
data_size > conn.send_window_size ->
1423-
throw({:mint, conn, wrap_error({:exceeds_window_size, :connection, conn.send_window_size})})
1447+
throw(
1448+
{:mint, conn, wrap_error({:exceeds_window_size, :connection, conn.send_window_size})}
1449+
)
14241450

14251451
# If the data size is greater than the max frame size, we chunk automatically based
14261452
# on the max frame size.
@@ -1790,12 +1816,12 @@ defmodule Mint.HTTP2 do
17901816
# roughly one update per `(receive_window_size - threshold)` bytes
17911817
# consumed.
17921818
defp refill_client_windows(conn, stream_id, data_size) do
1793-
conn = update_in(conn.receive_window, &(&1 - data_size))
1819+
conn = update_in(conn.receive_window_remaining, &(&1 - data_size))
17941820

17951821
conn =
17961822
case Map.fetch(conn.streams, stream_id) do
17971823
{:ok, _stream} ->
1798-
update_in(conn.streams[stream_id].receive_window, &(&1 - data_size))
1824+
update_in(conn.streams[stream_id].receive_window_remaining, &(&1 - data_size))
17991825

18001826
:error ->
18011827
conn
@@ -1815,8 +1841,8 @@ defmodule Mint.HTTP2 do
18151841
end
18161842

18171843
defp maybe_refill_conn(frames, conn) do
1818-
if conn.receive_window <= conn.receive_window_update_threshold do
1819-
increment = conn.receive_window_size - conn.receive_window
1844+
if conn.receive_window_remaining <= conn.receive_window_update_threshold do
1845+
increment = conn.receive_window_size - conn.receive_window_remaining
18201846
[window_update(stream_id: 0, window_size_increment: increment) | frames]
18211847
else
18221848
frames
@@ -1826,8 +1852,8 @@ defmodule Mint.HTTP2 do
18261852
defp maybe_refill_stream(frames, conn, stream_id) do
18271853
case Map.fetch(conn.streams, stream_id) do
18281854
{:ok, stream} ->
1829-
if stream.receive_window <= conn.receive_window_update_threshold do
1830-
increment = stream.receive_window_size - stream.receive_window
1855+
if stream.receive_window_remaining <= conn.receive_window_update_threshold do
1856+
increment = stream.receive_window_size - stream.receive_window_remaining
18311857

18321858
[
18331859
window_update(stream_id: stream_id, window_size_increment: increment) | frames
@@ -1844,11 +1870,11 @@ defmodule Mint.HTTP2 do
18441870
defp apply_refills(conn, frames) do
18451871
Enum.reduce(frames, conn, fn
18461872
window_update(stream_id: 0), conn ->
1847-
put_in(conn.receive_window, conn.receive_window_size)
1873+
put_in(conn.receive_window_remaining, conn.receive_window_size)
18481874

18491875
window_update(stream_id: stream_id), conn ->
18501876
put_in(
1851-
conn.streams[stream_id].receive_window,
1877+
conn.streams[stream_id].receive_window_remaining,
18521878
conn.streams[stream_id].receive_window_size
18531879
)
18541880
end)
@@ -2199,7 +2225,7 @@ defmodule Mint.HTTP2 do
21992225
state: :reserved_remote,
22002226
send_window_size: conn.server_settings.initial_window_size,
22012227
receive_window_size: conn.client_settings.initial_window_size,
2202-
receive_window: conn.client_settings.initial_window_size,
2228+
receive_window_remaining: conn.client_settings.initial_window_size,
22032229
received_first_headers?: false
22042230
}
22052231

test/mint/http2/conn_test.exs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ defmodule Mint.HTTP2Test do
131131
assert_http2_error error, {:protocol_error, "received invalid frame ping during handshake"}
132132
refute HTTP2.open?(conn)
133133
end
134-
135134
end
136135

137136
describe "set_window_size/3" do
@@ -141,12 +140,12 @@ defmodule Mint.HTTP2Test do
141140
%{conn: conn} do
142141
assert HTTP2.get_window_size(conn, :connection) == 65_535
143142
assert conn.receive_window_size == 65_535
144-
assert conn.receive_window == 65_535
143+
assert conn.receive_window_remaining == 65_535
145144

146145
assert {:ok, conn} = HTTP2.set_window_size(conn, :connection, 1_000_000)
147146

148147
assert conn.receive_window_size == 1_000_000
149-
assert conn.receive_window == 1_000_000
148+
assert conn.receive_window_remaining == 1_000_000
150149

151150
assert_recv_frames [
152151
window_update(stream_id: 0, window_size_increment: 934_465)
@@ -159,12 +158,12 @@ defmodule Mint.HTTP2Test do
159158
assert_recv_frames [headers(stream_id: stream_id)]
160159

161160
current = conn.streams[stream_id].receive_window_size
162-
assert conn.streams[stream_id].receive_window == current
161+
assert conn.streams[stream_id].receive_window_remaining == current
163162

164163
assert {:ok, conn} = HTTP2.set_window_size(conn, {:request, ref}, current + 10_000)
165164

166165
assert conn.streams[stream_id].receive_window_size == current + 10_000
167-
assert conn.streams[stream_id].receive_window == current + 10_000
166+
assert conn.streams[stream_id].receive_window_remaining == current + 10_000
168167

169168
assert_recv_frames [
170169
window_update(stream_id: ^stream_id, window_size_increment: 10_000)
@@ -1878,7 +1877,7 @@ defmodule Mint.HTTP2Test do
18781877
# a WINDOW_UPDATE for `16 MB - 65_535` so the server sees the peak
18791878
# from the start.
18801879
assert conn.receive_window_size == 16 * 1024 * 1024
1881-
assert conn.receive_window == 16 * 1024 * 1024
1880+
assert conn.receive_window_remaining == 16 * 1024 * 1024
18821881
end
18831882

18841883
test "advertises the configured stream window via SETTINGS", %{conn: conn} do
@@ -1889,13 +1888,41 @@ defmodule Mint.HTTP2Test do
18891888
assert_recv_frames [headers(stream_id: stream_id)]
18901889

18911890
assert conn.streams[stream_id].receive_window_size == 4 * 1024 * 1024
1892-
assert conn.streams[stream_id].receive_window == 4 * 1024 * 1024
1891+
assert conn.streams[stream_id].receive_window_remaining == 4 * 1024 * 1024
18931892
end
18941893

18951894
@tag connect_options: [connection_window_size: 1_000_000]
18961895
test "supports a custom :connection_window_size", %{conn: conn} do
18971896
assert conn.receive_window_size == 1_000_000
1898-
assert conn.receive_window == 1_000_000
1897+
assert conn.receive_window_remaining == 1_000_000
1898+
end
1899+
1900+
@tag :no_connection
1901+
test "streams opened before the SETTINGS ACK track the advertised window, not the default",
1902+
%{server_port: port, server_socket_task: server_socket_task} do
1903+
# Regression: when the user advertises a stream window smaller than
1904+
# the library default (4 MB), streams opened before the server's
1905+
# SETTINGS ACK must track the advertised value. Otherwise the
1906+
# client holds onto credit the server never granted, never crosses
1907+
# the refill threshold, never sends a stream-level WINDOW_UPDATE,
1908+
# and the connection stalls after the server exhausts its send
1909+
# window.
1910+
assert {:ok, conn} =
1911+
HTTP2.connect(:https, "localhost", port,
1912+
transport_opts: [verify: :verify_none],
1913+
client_settings: [initial_window_size: 65_535]
1914+
)
1915+
1916+
{:ok, _server_socket} = Task.await(server_socket_task)
1917+
1918+
# Open a request *before* the server has ACKed our SETTINGS — this
1919+
# is the pre-ACK window where the struct must already reflect what
1920+
# was advertised, not the library default.
1921+
assert {:ok, conn, ref} = HTTP2.request(conn, "GET", "/", [], nil)
1922+
1923+
stream_id = conn.ref_to_stream_id[ref]
1924+
assert conn.streams[stream_id].receive_window_size == 65_535
1925+
assert conn.streams[stream_id].receive_window_remaining == 65_535
18991926
end
19001927

19011928
@tag connect_options: [connection_window_size: 65_535]
@@ -1904,7 +1931,7 @@ defmodule Mint.HTTP2Test do
19041931
# At 65_535 there's nothing to advertise beyond SETTINGS — the
19051932
# preface should not carry an extra WINDOW_UPDATE.
19061933
assert conn.receive_window_size == 65_535
1907-
assert conn.receive_window == 65_535
1934+
assert conn.receive_window_remaining == 65_535
19081935
end
19091936

19101937
test "rejects a :connection_window_size below the spec minimum" do

0 commit comments

Comments
 (0)