Skip to content

Commit b12688c

Browse files
authored
http2: add new connectionWindowSize option
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #65619 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yongsheng Zhang <zyszys98@gmail.com>
1 parent 8c9d141 commit b12688c

8 files changed

Lines changed: 218 additions & 47 deletions

File tree

‎doc/api/http2.md‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,10 +669,16 @@ added:
669669

670670
* `windowSize` {number}
671671

672-
Sets the local endpoint's window size.
672+
Sets the local endpoint's connection-level window size.
673673
The `windowSize` is the total window size to set, not
674674
the delta.
675675

676+
Increases take effect immediately, but decreases only apply as the window
677+
already advertised to the peer is consumed, since a window that has been
678+
advertised cannot be retracted. To use a window smaller than the default from
679+
the start of the connection, set the `connectionWindowSize` option when
680+
creating the server or client session instead.
681+
676682
```mjs
677683
import { createServer } from 'node:http2';
678684

@@ -2844,6 +2850,9 @@ Throws `ERR_INVALID_ARG_TYPE` for invalid `settings` argument.
28442850
<!-- YAML
28452851
added: v8.4.0
28462852
changes:
2853+
- version: REPLACEME
2854+
pr-url: https://github.com/nodejs/node/pull/65619
2855+
description: Added the `connectionWindowSize` option.
28472856
- version:
28482857
- v25.7.0
28492858
- v24.15.0
@@ -2961,6 +2970,13 @@ changes:
29612970
error that should tell the peer to not open any more streams, continuing
29622971
to open streams is therefore regarded as a sign of a misbehaving peer.
29632972
**Default:** `100`.
2973+
* `connectionWindowSize` {number} Sets the initial flow control window for
2974+
each session, in bytes. This is the total amount of data the remote peer
2975+
may send across all streams before it has to wait for a `WINDOW_UPDATE`.
2976+
The equivalent per-stream limit is `settings.initialWindowSize`. The
2977+
minimum allowed value is `1` and the maximum is 2<sup>31</sup>-1. Values
2978+
below 65535 will not take effect until the initial protocol-default
2979+
window of 65535 has been used.
29642980
* `settings` {HTTP/2 Settings Object} The initial settings to send to the
29652981
remote peer upon connection.
29662982
* `streamResetBurst` {number} and `streamResetRate` {number} Sets the rate
@@ -3072,6 +3088,9 @@ server.listen(8000);
30723088
<!-- YAML
30733089
added: v8.4.0
30743090
changes:
3091+
- version: REPLACEME
3092+
pr-url: https://github.com/nodejs/node/pull/65619
3093+
description: Added the `connectionWindowSize` option.
30753094
- version:
30763095
- v25.7.0
30773096
- v24.15.0
@@ -3180,6 +3199,14 @@ changes:
31803199
error that should tell the peer to not open any more streams, continuing
31813200
to open streams is therefore regarded as a sign of a misbehaving peer.
31823201
**Default:** `100`.
3202+
* `connectionWindowSize` {number} Sets the initial flow control window for
3203+
each session, in bytes. This is the total amount of data the remote peer
3204+
may send across all streams before it has to wait for a `WINDOW_UPDATE`.
3205+
The equivalent per-stream limit is `settings.initialWindowSize`. The
3206+
minimum allowed value is `1` and the maximum is 2<sup>31</sup>-1. Values
3207+
below 65535 will not take effect until the initial protocol-default
3208+
window of 65535 has been used.
3209+
**Default:** `33554432`.
31833210
* `settings` {HTTP/2 Settings Object} The initial settings to send to the
31843211
remote peer upon connection.
31853212
* `streamResetBurst` {number} and `streamResetRate` {number} Sets the rate
@@ -3276,6 +3303,9 @@ server.listen(8443);
32763303
<!-- YAML
32773304
added: v8.4.0
32783305
changes:
3306+
- version: REPLACEME
3307+
pr-url: https://github.com/nodejs/node/pull/65619
3308+
description: Added the `connectionWindowSize` option.
32793309
- version:
32803310
- v15.10.0
32813311
- v14.16.0
@@ -3363,6 +3393,14 @@ changes:
33633393
* `protocol` {string} The protocol to connect with, if not set in the
33643394
`authority`. Value may be either `'http:'` or `'https:'`. **Default:**
33653395
`'https:'`
3396+
* `connectionWindowSize` {number} Sets the initial flow control window for
3397+
this session, in bytes. This is the total amount of data the remote peer
3398+
may send across all streams before it has to wait for a `WINDOW_UPDATE`.
3399+
The equivalent per-stream limit is `settings.initialWindowSize`. The
3400+
minimum allowed value is `1` and the maximum is 2<sup>31</sup>-1. Values
3401+
below 65535 will not take effect until the initial protocol-default
3402+
window of 65535 has been used.
3403+
**Default:** `33554432`.
33663404
* `settings` {HTTP/2 Settings Object} The initial settings to send to the
33673405
remote peer upon connection.
33683406
* `remoteCustomSettings` {Array} The array of integer values determines the
@@ -3795,6 +3833,9 @@ properties.
37953833
* `initialWindowSize` {number} Specifies the _sender's_ initial window size in
37963834
bytes for stream-level flow control. The minimum allowed value is 0. The
37973835
maximum allowed value is 2<sup>32</sup>-1. **Default:** `4194304`.
3836+
This is a per-stream limit; the window for the connection as a whole is
3837+
configured separately with the `connectionWindowSize` option of
3838+
[`http2.createServer()`][] or [`http2.connect()`][].
37983839
* `maxFrameSize` {number} Specifies the size in bytes of the largest frame
37993840
payload. The minimum allowed value is 16,384. The maximum allowed value is
38003841
2<sup>24</sup>-1. **Default:** `16384`.
@@ -5258,6 +5299,7 @@ you need to implement any fall-back behavior yourself.
52585299
[`http.createServer()`]: http.md#httpcreateserveroptions-requestlistener
52595300
[`http2.SecureServer`]: #class-http2secureserver
52605301
[`http2.Server`]: #class-http2server
5302+
[`http2.connect()`]: #http2connectauthority-options-listener
52615303
[`http2.createSecureServer()`]: #http2createsecureserveroptions-onrequesthandler
52625304
[`http2.createServer()`]: #http2createserveroptions-onrequesthandler
52635305
[`http2stream.pushStream()`]: #http2streampushstreamheaders-options-callback

‎lib/internal/http2/core.js‎

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ function debugSessionObj(session, message, ...args) {
241241
const kMaxFrameSize = (2 ** 24) - 1;
242242
const kMaxInt = (2 ** 32) - 1;
243243
const kMaxInitialWindowSize = (2 ** 31) - 1; // HTTP/2 spec maximum
244+
// A connection window of 0 stalls the connection
245+
const kMinConnectionWindowSize = 1;
244246
const kMaxStreams = (2 ** 32) - 1;
245247
const kMaxALTSVC = (2 ** 14) - 2;
246248

@@ -3590,19 +3592,38 @@ function connectionListener(socket) {
35903592
this.emit('session', session);
35913593
}
35923594

3593-
function initializeOptions(options) {
3594-
assertIsObject(options, 'options');
3595-
options = { ...options };
3596-
assertIsObject(options.settings, 'options.settings');
3597-
options.settings = { ...options.settings };
3598-
3595+
// Validates the session options that apply to both client and server sessions
3596+
function validateSessionOptions(options) {
35993597
assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings');
36003598
if (options.remoteCustomSettings) {
36013599
options.remoteCustomSettings = [ ...options.remoteCustomSettings ];
36023600
if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS)
36033601
throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
36043602
}
36053603

3604+
if (options.strictSingleValueFields !== undefined) {
3605+
validateBoolean(
3606+
options.strictSingleValueFields,
3607+
'options.strictSingleValueFields',
3608+
);
3609+
} else {
3610+
options.strictSingleValueFields = true;
3611+
}
3612+
3613+
if (options.connectionWindowSize !== undefined) {
3614+
validateInt32(options.connectionWindowSize, 'options.connectionWindowSize',
3615+
kMinConnectionWindowSize, kMaxInitialWindowSize);
3616+
}
3617+
}
3618+
3619+
function initializeOptions(options) {
3620+
assertIsObject(options, 'options');
3621+
options = { ...options };
3622+
assertIsObject(options.settings, 'options.settings');
3623+
options.settings = { ...options.settings };
3624+
3625+
validateSessionOptions(options);
3626+
36063627
if (options.maxSessionInvalidFrames !== undefined)
36073628
validateUint32(options.maxSessionInvalidFrames, 'options.maxSessionInvalidFrames');
36083629

@@ -3619,16 +3640,6 @@ function initializeOptions(options) {
36193640
// TODO(danbev): is this a good default value?
36203641
options.unknownProtocolTimeout = 10000;
36213642

3622-
if (options.strictSingleValueFields !== undefined) {
3623-
validateBoolean(
3624-
options.strictSingleValueFields,
3625-
'options.strictSingleValueFields',
3626-
);
3627-
} else {
3628-
options.strictSingleValueFields = true;
3629-
}
3630-
3631-
36323643
// Initialize http1Options bag for HTTP/1 fallback when allowHTTP1 is true.
36333644
// This bag is passed to storeHTTPOptions() to configure HTTP/1 server
36343645
// behavior (timeouts, IncomingMessage/ServerResponse classes, etc.).
@@ -3840,21 +3851,7 @@ function connect(authority, options, listener) {
38403851
maxOriginSetSize = 128;
38413852
}
38423853

3843-
assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings');
3844-
if (options.remoteCustomSettings) {
3845-
options.remoteCustomSettings = [ ...options.remoteCustomSettings ];
3846-
if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS)
3847-
throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
3848-
}
3849-
3850-
if (options.strictSingleValueFields !== undefined) {
3851-
validateBoolean(
3852-
options.strictSingleValueFields,
3853-
'options.strictSingleValueFields',
3854-
);
3855-
} else {
3856-
options.strictSingleValueFields = true;
3857-
}
3854+
validateSessionOptions(options);
38583855

38593856
if (typeof authority === 'string')
38603857
authority = new URL(authority);

‎lib/internal/http2/util.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ const IDX_OPTIONS_MAX_SETTINGS = 9;
233233
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
234234
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
235235
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
236-
const IDX_OPTIONS_FLAGS = 13;
236+
const IDX_OPTIONS_CONNECTION_WINDOW_SIZE = 13;
237+
const IDX_OPTIONS_FLAGS = 14;
237238

238239
function updateOptionsBuffer(options) {
239240
let flags = 0;
@@ -303,6 +304,11 @@ function updateOptionsBuffer(options) {
303304
optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION] =
304305
options.strictFieldWhitespaceValidation === true ? 0 : 1;
305306
}
307+
if (typeof options.connectionWindowSize === 'number') {
308+
flags |= (1 << IDX_OPTIONS_CONNECTION_WINDOW_SIZE);
309+
optionsBuffer[IDX_OPTIONS_CONNECTION_WINDOW_SIZE] =
310+
options.connectionWindowSize;
311+
}
306312

307313
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
308314
}

‎src/node_http2.cc‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ Http2Options::Http2Options(Http2State* http2_state, SessionType type) {
217217
static_cast<size_t>(buffer[IDX_OPTIONS_MAX_SETTINGS]));
218218
}
219219

220+
if (flags & (1 << IDX_OPTIONS_CONNECTION_WINDOW_SIZE)) {
221+
set_connection_window_size(
222+
static_cast<int32_t>(buffer[IDX_OPTIONS_CONNECTION_WINDOW_SIZE]));
223+
}
224+
220225
if ((flags & (1 << IDX_OPTIONS_STREAM_RESET_BURST)) &&
221226
(flags & (1 << IDX_OPTIONS_STREAM_RESET_RATE))) {
222227
nghttp2_option_set_stream_reset_rate_limit(
@@ -612,15 +617,13 @@ Http2Session::Http2Session(Http2State* http2_state,
612617
&alloc_info), 0);
613618
session_.reset(session);
614619

615-
// Increase the default local connection window to improve throughput
616-
// on high-latency connections. The default 64KB window limits throughput
617-
// to window_size / RTT. With a 32MB connection window, throughput is
618-
// significantly improved. See https://github.com/nodejs/node/issues/38426
620+
// The default connection window is larger than the 64KB required by the
621+
// spec to improve throughput on high-latency connections, where throughput
622+
// is limited to window_size / RTT. User settings have to be applied here
623+
// initially rather than updating later as windows cannot be shrunk after
624+
// they've been advertised.
619625
CHECK_EQ(nghttp2_session_set_local_window_size(
620-
session,
621-
NGHTTP2_FLAG_NONE,
622-
0,
623-
DEFAULT_SETTINGS_LOCAL_CONNECTION_WINDOW_SIZE),
626+
session, NGHTTP2_FLAG_NONE, 0, opts.connection_window_size()),
624627
0);
625628

626629
outgoing_storage_.reserve(1024);

‎src/node_http2.h‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ constexpr size_t kDefaultMaxSettings = 10;
3939
// Default maximum total memory cap for Http2Session.
4040
constexpr uint64_t kDefaultMaxSessionMemory = 10000000;
4141

42+
// Default connection-level flow control window (32MB) to improve throughput
43+
// on high-latency connections. See https://github.com/nodejs/node/issues/38426
44+
constexpr int32_t kDefaultConnectionWindowSize = 33554432;
45+
4246
// These are the standard HTTP/2 defaults as specified by the RFC
4347
constexpr uint32_t DEFAULT_SETTINGS_HEADER_TABLE_SIZE = 4096;
4448
constexpr uint32_t DEFAULT_SETTINGS_ENABLE_PUSH = 1;
@@ -51,10 +55,6 @@ constexpr uint32_t MAX_MAX_FRAME_SIZE = 16777215;
5155
constexpr uint32_t MIN_MAX_FRAME_SIZE = DEFAULT_SETTINGS_MAX_FRAME_SIZE;
5256
constexpr uint32_t MAX_INITIAL_WINDOW_SIZE = 2147483647;
5357

54-
// Default local connection window size (32MB) to improve throughput
55-
// on high-latency connections. See https://github.com/nodejs/node/issues/38426
56-
constexpr uint32_t DEFAULT_SETTINGS_LOCAL_CONNECTION_WINDOW_SIZE = 33554432;
57-
5858
// Stream is not going to have any DATA frames
5959
constexpr int STREAM_OPTION_EMPTY_PAYLOAD = 0x1;
6060

@@ -245,9 +245,16 @@ class Http2Options {
245245
return max_session_memory_;
246246
}
247247

248+
void set_connection_window_size(int32_t size) {
249+
connection_window_size_ = size;
250+
}
251+
252+
int32_t connection_window_size() const { return connection_window_size_; }
253+
248254
private:
249255
Nghttp2OptionPointer options_;
250256
uint64_t max_session_memory_ = kDefaultMaxSessionMemory;
257+
int32_t connection_window_size_ = kDefaultConnectionWindowSize;
251258
uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
252259
PaddingStrategy padding_strategy_ = PADDING_STRATEGY_NONE;
253260
size_t max_outstanding_pings_ = kDefaultMaxPings;

‎src/node_http2_state.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ namespace http2 {
6161
IDX_OPTIONS_STREAM_RESET_RATE,
6262
IDX_OPTIONS_STREAM_RESET_BURST,
6363
IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION,
64+
IDX_OPTIONS_CONNECTION_WINDOW_SIZE,
6465
IDX_OPTIONS_FLAGS
6566
};
6667

0 commit comments

Comments
 (0)