@@ -176,7 +176,16 @@ defmodule Mint.HTTP2 do
176176
177177 # Fields of the connection.
178178 buffer: "" ,
179+ # `window_size` is the client *send* window for the connection — how
180+ # much request-body data we're allowed to send to the server before it
181+ # refills the window with a WINDOW_UPDATE frame.
179182 window_size: @ default_window_size ,
183+ # `receive_window_size` is the client *receive* window for the
184+ # connection — the peak size we've advertised to the server via
185+ # `WINDOW_UPDATE` frames on stream 0 (or the spec default of 65_535
186+ # if we've never sent one). Mint auto-refills to maintain this peak
187+ # as DATA frames arrive.
188+ receive_window_size: @ default_window_size ,
180189 encode_table: HPAX . new ( 4096 ) ,
181190 decode_table: HPAX . new ( 4096 ) ,
182191
@@ -729,11 +738,21 @@ defmodule Mint.HTTP2 do
729738 end
730739
731740 @ doc """
732- Returns the window size of the connection or of a single request.
741+ Returns the client **send** window size for the connection or a request.
733742
734- This function is HTTP/2 specific. It returns the window size of
735- either the connection if `connection_or_request` is `:connection` or of a single
736- request if `connection_or_request` is `{:request, request_ref}`.
743+ > #### Send vs receive windows {: .warning}
744+ >
745+ > This function returns the *send* window — how much body data this client
746+ > is still permitted to send to the server before being throttled. It is
747+ > decremented by `request/5` and `stream_request_body/3` and refilled by
748+ > the server, which `stream/2` handles transparently.
749+ >
750+ > It does **not** return the client *receive* window (how much the server
751+ > is permitted to send us). To influence that, use `set_window_size/3`.
752+
753+ This function is HTTP/2 specific. It returns the send window of either the
754+ connection if `connection_or_request` is `:connection` or of a single request
755+ if `connection_or_request` is `{:request, request_ref}`.
737756
738757 Use this function to check the window size of the connection before sending a
739758 full request. Also use this function to check the window size of both the
@@ -744,21 +763,23 @@ defmodule Mint.HTTP2 do
744763
745764 ## HTTP/2 Flow Control
746765
747- In HTTP/2, flow control is implemented through a
748- window size. When the client sends data to the server, the window size is decreased
749- and the server needs to "refill" it on the client side. You don't need to take care of
750- the refilling of the client window as it happens behind the scenes in `stream/2`.
766+ In HTTP/2, flow control is implemented through a window size. When the client
767+ sends data to the server, the window size is decreased and the server needs
768+ to "refill" it on the client side, which `stream/2` handles transparently.
769+ Symmetrically, the server's outbound flow toward the client is bounded by a
770+ receive window the client advertises and refills — see `set_window_size/3`.
751771
752- A window size is kept for the entire connection and all requests affect this window
753- size. A window size is also kept per request.
772+ A window size is kept for the entire connection and all requests affect this
773+ window size. A window size is also kept per request.
754774
755- The only thing that affects the window size is the body of a request, regardless of
756- if it's a full request sent with `request/5` or body chunks sent through
757- `stream_request_body/3`. That means that if we make a request with a body that is
758- five bytes long, like `"hello"`, the window size of the connection and the window size
759- of that particular request will decrease by five bytes.
775+ The only thing that affects the send window size is the body of a request,
776+ regardless of whether it's a full request sent with `request/5` or body chunks
777+ sent through `stream_request_body/3`. That means that if we make a request with
778+ a body that is five bytes long, like `"hello"`, the send window size of the
779+ connection and the send window size of that particular request will decrease
780+ by five bytes.
760781
761- If we use all the window size before the server refills it, functions like
782+ If we use all the send window size before the server refills it, functions like
762783 `request/5` will return an error.
763784
764785 ## Examples
@@ -797,6 +818,119 @@ defmodule Mint.HTTP2 do
797818 end
798819 end
799820
821+ @ doc """
822+ Advertises a larger client **receive** window to the server.
823+
824+ > #### Receive vs send windows {: .warning}
825+ >
826+ > This function sets the *receive* window — the peak amount of body data
827+ > the server is permitted to send us before being throttled. It does
828+ > **not** set the *send* window (how much body data we're permitted to
829+ > send to the server) — the server controls that. See `get_window_size/2`
830+ > for the send window.
831+
832+ Without calling this, `stream/2` refills the receive window in small
833+ increments as response body data is consumed. Each refill costs a
834+ round-trip before the server can send more, so bulk throughput is capped
835+ at roughly `window / RTT`; on higher-latency links the default 64 KB
836+ window makes that cap well below the link bandwidth. Raising the window
837+ removes those pauses and is the main HTTP/2 tuning knob for bulk or
838+ highly parallel downloads.
839+
840+ Mint exposes the per-stream initial window as the `:initial_window_size`
841+ client setting passed to `connect/4`, but there is no connection-level
842+ equivalent — use this function for the connection window, and for any
843+ per-stream adjustment after a request has started.
844+
845+ `connection_or_request` is `:connection` for the whole connection or
846+ `{:request, request_ref}` for a single request. `new_size` must be in
847+ `1..2_147_483_647`. Windows can only grow: `new_size` smaller than the
848+ current receive window returns
849+ `{:error, conn, %Mint.HTTPError{reason: :window_size_too_small}}`, and
850+ `new_size` equal to the current window is a no-op.
851+
852+ For more information on flow control and window sizes in HTTP/2, see the
853+ section below.
854+
855+ ## HTTP/2 Flow Control
856+
857+ See `get_window_size/2` for a description of the client *send* window.
858+ The client *receive* window is the symmetric bound on the server's
859+ outbound flow: it starts at 64 KB for the connection and for each new
860+ request, is decremented by response body bytes, and is refilled by
861+ `stream/2` as the body is consumed. A window size is kept for the entire
862+ connection and all responses affect this window size; a window size is
863+ also kept per request.
864+
865+ This function raises the *advertised* receive window — the peak the
866+ server is allowed to fill before pausing. It does not pre-allocate any
867+ buffers; it only permits the server to send further ahead of the
868+ client's reads.
869+
870+ ## Examples
871+
872+ Bump the connection-level receive window right after connect so the server
873+ can stream multi-MB bodies without flow-control pauses:
874+
875+ {:ok, conn} = Mint.HTTP2.connect(:https, host, 443)
876+ {:ok, conn} = Mint.HTTP2.set_window_size(conn, :connection, 8_000_000)
877+
878+ Give one specific request a bigger window than the per-stream default:
879+
880+ {:ok, conn, ref} = Mint.HTTP2.request(conn, "GET", "/huge", [], nil)
881+ {:ok, conn} = Mint.HTTP2.set_window_size(conn, {:request, ref}, 16_000_000)
882+
883+ """
884+ @ spec set_window_size ( t ( ) , :connection | { :request , Types . request_ref ( ) } , pos_integer ( ) ) ::
885+ { :ok , t ( ) } | { :error , t ( ) , Types . error ( ) }
886+ def set_window_size ( conn , connection_or_request , new_size )
887+
888+ def set_window_size ( % __MODULE__ { } = _conn , _target , new_size )
889+ when not ( is_integer ( new_size ) and new_size >= 1 and new_size <= @ max_window_size ) do
890+ raise ArgumentError ,
891+ "new window size must be an integer in 1..#{ @ max_window_size } , got: #{ inspect ( new_size ) } "
892+ end
893+
894+ def set_window_size ( % __MODULE__ { } = conn , :connection , new_size ) do
895+ do_set_window_size ( conn , 0 , conn . receive_window_size , new_size , fn conn , size ->
896+ put_in ( conn . receive_window_size , size )
897+ end )
898+ catch
899+ :throw , { :mint , conn , error } -> { :error , conn , error }
900+ end
901+
902+ def set_window_size ( % __MODULE__ { } = conn , { :request , request_ref } , new_size ) do
903+ case Map . fetch ( conn . ref_to_stream_id , request_ref ) do
904+ { :ok , stream_id } ->
905+ current = conn . streams [ stream_id ] . receive_window_size
906+
907+ do_set_window_size ( conn , stream_id , current , new_size , fn conn , size ->
908+ put_in ( conn . streams [ stream_id ] . receive_window_size , size )
909+ end )
910+
911+ :error ->
912+ { :error , conn , wrap_error ( { :unknown_request_to_stream , request_ref } ) }
913+ end
914+ catch
915+ :throw , { :mint , conn , error } -> { :error , conn , error }
916+ end
917+
918+ defp do_set_window_size ( conn , _stream_id , current , new_size , _update )
919+ when new_size == current do
920+ { :ok , conn }
921+ end
922+
923+ defp do_set_window_size ( conn , _stream_id , current , new_size , _update ) when new_size < current do
924+ { :error , conn , wrap_error ( { :window_size_too_small , current , new_size } ) }
925+ end
926+
927+ defp do_set_window_size ( conn , stream_id , current , new_size , update ) do
928+ increment = new_size - current
929+ frame = window_update ( stream_id: stream_id , window_size_increment: increment )
930+ conn = send! ( conn , Frame . encode ( frame ) )
931+ { :ok , update . ( conn , new_size ) }
932+ end
933+
800934 @ doc """
801935 See `Mint.HTTP.stream/2`.
802936 """
@@ -1083,7 +1217,15 @@ defmodule Mint.HTTP2 do
10831217 id: conn . next_stream_id ,
10841218 ref: make_ref ( ) ,
10851219 state: :idle ,
1220+ # Client send window — decremented as we send body bytes, refilled
1221+ # by incoming WINDOW_UPDATE frames from the server. Bounded initially
1222+ # by the server's SETTINGS_INITIAL_WINDOW_SIZE.
10861223 window_size: conn . server_settings . initial_window_size ,
1224+ # Client receive window — the peak we've advertised to the server
1225+ # for this stream. Starts at whatever we told the server via our
1226+ # SETTINGS_INITIAL_WINDOW_SIZE; can be bumped per-stream with
1227+ # `set_window_size/3`.
1228+ receive_window_size: conn . client_settings . initial_window_size ,
10871229 received_first_headers?: false
10881230 }
10891231
@@ -1846,9 +1988,14 @@ defmodule Mint.HTTP2 do
18461988 end
18471989 end
18481990
1991+ valid_client_settings_without_iws = @ valid_client_settings -- [ :initial_window_size ]
1992+
18491993 defp apply_client_settings ( conn , client_settings ) do
18501994 Enum . reduce ( client_settings , conn , fn
1851- { setting , value } , conn when setting in @ valid_client_settings ->
1995+ { :initial_window_size , initial_window_size } , conn ->
1996+ update_client_initial_window_size ( conn , initial_window_size )
1997+
1998+ { setting , value } , conn when setting in unquote ( valid_client_settings_without_iws ) ->
18521999 update_in ( conn . client_settings , & % { & 1 | setting => value } )
18532000
18542001 { setting , _value } , _conn ->
@@ -1880,6 +2027,23 @@ defmodule Mint.HTTP2 do
18802027 put_in ( conn . server_settings . initial_window_size , new_iws )
18812028 end
18822029
2030+ defp update_client_initial_window_size ( conn , new_iws ) do
2031+ diff = new_iws - conn . client_settings . initial_window_size
2032+
2033+ conn =
2034+ update_in ( conn . streams , fn streams ->
2035+ for { stream_id , stream } <- streams ,
2036+ stream . state in [ :open , :half_closed_local , :reserved_remote ] ,
2037+ into: streams do
2038+ receive_window_size = stream . receive_window_size + diff
2039+
2040+ { stream_id , % { stream | receive_window_size: receive_window_size } }
2041+ end
2042+ end )
2043+
2044+ put_in ( conn . client_settings . initial_window_size , new_iws )
2045+ end
2046+
18832047 # PUSH_PROMISE
18842048
18852049 defp handle_push_promise (
@@ -1933,6 +2097,7 @@ defmodule Mint.HTTP2 do
19332097 ref: make_ref ( ) ,
19342098 state: :reserved_remote ,
19352099 window_size: conn . server_settings . initial_window_size ,
2100+ receive_window_size: conn . client_settings . initial_window_size ,
19362101 received_first_headers?: false
19372102 }
19382103
@@ -2223,6 +2388,15 @@ defmodule Mint.HTTP2 do
22232388 "can't stream chunk of data because the request is unknown"
22242389 end
22252390
2391+ def format_error ( { :unknown_request_to_stream , ref } ) do
2392+ "request with reference #{ inspect ( ref ) } was not found"
2393+ end
2394+
2395+ def format_error ( { :window_size_too_small , current , new_size } ) do
2396+ "set_window_size/3 can only grow a window; new size #{ new_size } is " <>
2397+ "smaller than the current size #{ current } "
2398+ end
2399+
22262400 def format_error ( :request_is_not_streaming ) do
22272401 "can't send more data on this request since it's not streaming"
22282402 end
0 commit comments