http2: add new connectionWindowSize option

Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65619
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yongsheng Zhang <zyszys98@gmail.com>
This commit is contained in:
Tim Perry authored and GitHub committed 2026-09-09 07:46:46 +00:00
1 parent 8c9d14118b
commit b12688c6e6
8 files changed
+218 -47

No files matched your search

+43 -1
View File
@@ -669,10 +669,16 @@ added:
* `windowSize` {number}
Sets the local endpoint's window size.
Sets the local endpoint's connection-level window size.
The `windowSize` is the total window size to set, not
the delta.
Increases take effect immediately, but decreases only apply as the window
already advertised to the peer is consumed, since a window that has been
advertised cannot be retracted. To use a window smaller than the default from
the start of the connection, set the `connectionWindowSize` option when
creating the server or client session instead.
```mjs
import { createServer } from 'node:http2';
@@ -2844,6 +2850,9 @@ Throws `ERR_INVALID_ARG_TYPE` for invalid `settings` argument.
<!-- YAML
added: v8.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65619
description: Added the `connectionWindowSize` option.
- version:
- v25.7.0
- v24.15.0
@@ -2961,6 +2970,13 @@ changes:
error that should tell the peer to not open any more streams, continuing
to open streams is therefore regarded as a sign of a misbehaving peer.
**Default:** `100`.
* `connectionWindowSize` {number} Sets the initial flow control window for
each session, in bytes. This is the total amount of data the remote peer
may send across all streams before it has to wait for a `WINDOW_UPDATE`.
The equivalent per-stream limit is `settings.initialWindowSize`. The
minimum allowed value is `1` and the maximum is 2<sup>31</sup>-1. Values
below 65535 will not take effect until the initial protocol-default
window of 65535 has been used.
* `settings` {HTTP/2 Settings Object} The initial settings to send to the
remote peer upon connection.
* `streamResetBurst` {number} and `streamResetRate` {number} Sets the rate
@@ -3072,6 +3088,9 @@ server.listen(8000);
<!-- YAML
added: v8.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65619
description: Added the `connectionWindowSize` option.
- version:
- v25.7.0
- v24.15.0
@@ -3180,6 +3199,14 @@ changes:
error that should tell the peer to not open any more streams, continuing
to open streams is therefore regarded as a sign of a misbehaving peer.
**Default:** `100`.
* `connectionWindowSize` {number} Sets the initial flow control window for
each session, in bytes. This is the total amount of data the remote peer
may send across all streams before it has to wait for a `WINDOW_UPDATE`.
The equivalent per-stream limit is `settings.initialWindowSize`. The
minimum allowed value is `1` and the maximum is 2<sup>31</sup>-1. Values
below 65535 will not take effect until the initial protocol-default
window of 65535 has been used.
**Default:** `33554432`.
* `settings` {HTTP/2 Settings Object} The initial settings to send to the
remote peer upon connection.
* `streamResetBurst` {number} and `streamResetRate` {number} Sets the rate
@@ -3276,6 +3303,9 @@ server.listen(8443);
<!-- YAML
added: v8.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65619
description: Added the `connectionWindowSize` option.
- version:
- v15.10.0
- v14.16.0
@@ -3363,6 +3393,14 @@ changes:
* `protocol` {string} The protocol to connect with, if not set in the
`authority`. Value may be either `'http:'` or `'https:'`. **Default:**
`'https:'`
* `connectionWindowSize` {number} Sets the initial flow control window for
this session, in bytes. This is the total amount of data the remote peer
may send across all streams before it has to wait for a `WINDOW_UPDATE`.
The equivalent per-stream limit is `settings.initialWindowSize`. The
minimum allowed value is `1` and the maximum is 2<sup>31</sup>-1. Values
below 65535 will not take effect until the initial protocol-default
window of 65535 has been used.
**Default:** `33554432`.
* `settings` {HTTP/2 Settings Object} The initial settings to send to the
remote peer upon connection.
* `remoteCustomSettings` {Array} The array of integer values determines the
@@ -3795,6 +3833,9 @@ properties.
* `initialWindowSize` {number} Specifies the _sender's_ initial window size in
bytes for stream-level flow control. The minimum allowed value is 0. The
maximum allowed value is 2<sup>32</sup>-1. **Default:** `4194304`.
This is a per-stream limit; the window for the connection as a whole is
configured separately with the `connectionWindowSize` option of
[`http2.createServer()`][] or [`http2.connect()`][].
* `maxFrameSize` {number} Specifies the size in bytes of the largest frame
payload. The minimum allowed value is 16,384. The maximum allowed value is
2<sup>24</sup>-1. **Default:** `16384`.
@@ -5258,6 +5299,7 @@ you need to implement any fall-back behavior yourself.
[`http.createServer()`]: http.md#httpcreateserveroptions-requestlistener
[`http2.SecureServer`]: #class-http2secureserver
[`http2.Server`]: #class-http2server
[`http2.connect()`]: #http2connectauthority-options-listener
[`http2.createSecureServer()`]: #http2createsecureserveroptions-onrequesthandler
[`http2.createServer()`]: #http2createserveroptions-onrequesthandler
[`http2stream.pushStream()`]: #http2streampushstreamheaders-options-callback
+28 -31
View File
@@ -241,6 +241,8 @@ function debugSessionObj(session, message, ...args) {
const kMaxFrameSize = (2 ** 24) - 1;
const kMaxInt = (2 ** 32) - 1;
const kMaxInitialWindowSize = (2 ** 31) - 1; // HTTP/2 spec maximum
// A connection window of 0 stalls the connection
const kMinConnectionWindowSize = 1;
const kMaxStreams = (2 ** 32) - 1;
const kMaxALTSVC = (2 ** 14) - 2;
@@ -3590,12 +3592,8 @@ function connectionListener(socket) {
this.emit('session', session);
}
function initializeOptions(options) {
assertIsObject(options, 'options');
options = { ...options };
assertIsObject(options.settings, 'options.settings');
options.settings = { ...options.settings };
// Validates the session options that apply to both client and server sessions
function validateSessionOptions(options) {
assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings');
if (options.remoteCustomSettings) {
options.remoteCustomSettings = [ ...options.remoteCustomSettings ];
@@ -3603,6 +3601,29 @@ function initializeOptions(options) {
throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
}
if (options.strictSingleValueFields !== undefined) {
validateBoolean(
options.strictSingleValueFields,
'options.strictSingleValueFields',
);
} else {
options.strictSingleValueFields = true;
}
if (options.connectionWindowSize !== undefined) {
validateInt32(options.connectionWindowSize, 'options.connectionWindowSize',
kMinConnectionWindowSize, kMaxInitialWindowSize);
}
}
function initializeOptions(options) {
assertIsObject(options, 'options');
options = { ...options };
assertIsObject(options.settings, 'options.settings');
options.settings = { ...options.settings };
validateSessionOptions(options);
if (options.maxSessionInvalidFrames !== undefined)
validateUint32(options.maxSessionInvalidFrames, 'options.maxSessionInvalidFrames');
@@ -3619,16 +3640,6 @@ function initializeOptions(options) {
// TODO(danbev): is this a good default value?
options.unknownProtocolTimeout = 10000;
if (options.strictSingleValueFields !== undefined) {
validateBoolean(
options.strictSingleValueFields,
'options.strictSingleValueFields',
);
} else {
options.strictSingleValueFields = true;
}
// Initialize http1Options bag for HTTP/1 fallback when allowHTTP1 is true.
// This bag is passed to storeHTTPOptions() to configure HTTP/1 server
// behavior (timeouts, IncomingMessage/ServerResponse classes, etc.).
@@ -3840,21 +3851,7 @@ function connect(authority, options, listener) {
maxOriginSetSize = 128;
}
assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings');
if (options.remoteCustomSettings) {
options.remoteCustomSettings = [ ...options.remoteCustomSettings ];
if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS)
throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
}
if (options.strictSingleValueFields !== undefined) {
validateBoolean(
options.strictSingleValueFields,
'options.strictSingleValueFields',
);
} else {
options.strictSingleValueFields = true;
}
validateSessionOptions(options);
if (typeof authority === 'string')
authority = new URL(authority);
+7 -1
View File
@@ -233,7 +233,8 @@ const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
const IDX_OPTIONS_FLAGS = 13;
const IDX_OPTIONS_CONNECTION_WINDOW_SIZE = 13;
const IDX_OPTIONS_FLAGS = 14;
function updateOptionsBuffer(options) {
let flags = 0;
@@ -303,6 +304,11 @@ function updateOptionsBuffer(options) {
optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION] =
options.strictFieldWhitespaceValidation === true ? 0 : 1;
}
if (typeof options.connectionWindowSize === 'number') {
flags |= (1 << IDX_OPTIONS_CONNECTION_WINDOW_SIZE);
optionsBuffer[IDX_OPTIONS_CONNECTION_WINDOW_SIZE] =
options.connectionWindowSize;
}
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
}
+11 -8
View File
@@ -217,6 +217,11 @@ Http2Options::Http2Options(Http2State* http2_state, SessionType type) {
static_cast<size_t>(buffer[IDX_OPTIONS_MAX_SETTINGS]));
}
if (flags & (1 << IDX_OPTIONS_CONNECTION_WINDOW_SIZE)) {
set_connection_window_size(
static_cast<int32_t>(buffer[IDX_OPTIONS_CONNECTION_WINDOW_SIZE]));
}
if ((flags & (1 << IDX_OPTIONS_STREAM_RESET_BURST)) &&
(flags & (1 << IDX_OPTIONS_STREAM_RESET_RATE))) {
nghttp2_option_set_stream_reset_rate_limit(
@@ -612,15 +617,13 @@ Http2Session::Http2Session(Http2State* http2_state,
&alloc_info), 0);
session_.reset(session);
// Increase the default local connection window to improve throughput
// on high-latency connections. The default 64KB window limits throughput
// to window_size / RTT. With a 32MB connection window, throughput is
// significantly improved. See https://github.com/nodejs/node/issues/38426
// The default connection window is larger than the 64KB required by the
// spec to improve throughput on high-latency connections, where throughput
// is limited to window_size / RTT. User settings have to be applied here
// initially rather than updating later as windows cannot be shrunk after
// they've been advertised.
CHECK_EQ(nghttp2_session_set_local_window_size(
session,
NGHTTP2_FLAG_NONE,
0,
DEFAULT_SETTINGS_LOCAL_CONNECTION_WINDOW_SIZE),
session, NGHTTP2_FLAG_NONE, 0, opts.connection_window_size()),
0);
outgoing_storage_.reserve(1024);
+11 -4
View File
@@ -39,6 +39,10 @@ constexpr size_t kDefaultMaxSettings = 10;
// Default maximum total memory cap for Http2Session.
constexpr uint64_t kDefaultMaxSessionMemory = 10000000;
// Default connection-level flow control window (32MB) to improve throughput
// on high-latency connections. See https://github.com/nodejs/node/issues/38426
constexpr int32_t kDefaultConnectionWindowSize = 33554432;
// These are the standard HTTP/2 defaults as specified by the RFC
constexpr uint32_t DEFAULT_SETTINGS_HEADER_TABLE_SIZE = 4096;
constexpr uint32_t DEFAULT_SETTINGS_ENABLE_PUSH = 1;
@@ -51,10 +55,6 @@ constexpr uint32_t MAX_MAX_FRAME_SIZE = 16777215;
constexpr uint32_t MIN_MAX_FRAME_SIZE = DEFAULT_SETTINGS_MAX_FRAME_SIZE;
constexpr uint32_t MAX_INITIAL_WINDOW_SIZE = 2147483647;
// Default local connection window size (32MB) to improve throughput
// on high-latency connections. See https://github.com/nodejs/node/issues/38426
constexpr uint32_t DEFAULT_SETTINGS_LOCAL_CONNECTION_WINDOW_SIZE = 33554432;
// Stream is not going to have any DATA frames
constexpr int STREAM_OPTION_EMPTY_PAYLOAD = 0x1;
@@ -245,9 +245,16 @@ class Http2Options {
return max_session_memory_;
}
void set_connection_window_size(int32_t size) {
connection_window_size_ = size;
}
int32_t connection_window_size() const { return connection_window_size_; }
private:
Nghttp2OptionPointer options_;
uint64_t max_session_memory_ = kDefaultMaxSessionMemory;
int32_t connection_window_size_ = kDefaultConnectionWindowSize;
uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
PaddingStrategy padding_strategy_ = PADDING_STRATEGY_NONE;
size_t max_outstanding_pings_ = kDefaultMaxPings;
+1
View File
@@ -61,6 +61,7 @@ namespace http2 {
IDX_OPTIONS_STREAM_RESET_RATE,
IDX_OPTIONS_STREAM_RESET_BURST,
IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION,
IDX_OPTIONS_CONNECTION_WINDOW_SIZE,
IDX_OPTIONS_FLAGS
};
@@ -0,0 +1,108 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const DEFAULT_WINDOW = 33554432;
const SPEC_DEFAULT_WINDOW = 65535;
// Set client & server's windows (or default if undefined) and validate that
// the both peers see the remote value as expected.
function check(serverWindow, clientWindow) {
const server = http2.createServer({ connectionWindowSize: serverWindow });
serverWindow ??= DEFAULT_WINDOW;
clientWindow ??= DEFAULT_WINDOW;
server.on('session', common.mustCall((session) => {
assert.strictEqual(session.state.effectiveLocalWindowSize, serverWindow);
}));
server.on('stream', common.mustCall((stream) => {
assert.strictEqual(stream.session.state.remoteWindowSize, clientWindow);
stream.respond({ ':status': 200 }, { endStream: true });
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`,
{ connectionWindowSize: clientWindow });
client.on('connect', common.mustCall(() => {
assert.strictEqual(client.state.effectiveLocalWindowSize, clientWindow);
}));
const req = client.request();
req.resume();
req.on('close', common.mustCall(() => {
assert.strictEqual(client.state.remoteWindowSize, serverWindow);
client.close();
server.close();
}));
}));
}
check(undefined, undefined);
// The option shrinks the window below the default, which setLocalWindowSize()
// cannot do once the connection has been established.
check(SPEC_DEFAULT_WINDOW, SPEC_DEFAULT_WINDOW);
// Each end of the connection is configured independently.
check(2 ** 20, 2 ** 21);
// A window below the peer's fixed initial 65535 is the only case that shrinks
// the window nghttp2 has already accounted for, leaving a reduction to pay off
// before any WINDOW_UPDATE can be sent again. Larger windows, including the
// default, only ever grow it. Send more than the window to prove the
// connection still resumes rather than stalling once the reduction is paid.
{
const windowSize = 16384;
const payload = Buffer.alloc(2 ** 18, 'x');
const server = http2.createServer({ connectionWindowSize: windowSize });
server.on('stream', common.mustCall((stream) => {
let received = 0;
stream.on('data', (chunk) => { received += chunk.length; });
stream.on('end', common.mustCall(() => {
assert.strictEqual(received, payload.length);
stream.respond({ ':status': 200 }, { endStream: true });
}));
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`,
{ connectionWindowSize: windowSize });
const req = client.request({ ':method': 'POST', ':path': '/' });
req.resume();
req.end(payload);
req.on('close', common.mustCall(() => {
client.close();
server.close();
}));
}));
}
for (const connectionWindowSize of [-1, 0, 2 ** 31, 1.5]) {
assert.throws(() => http2.createServer({ connectionWindowSize }),
{ code: 'ERR_OUT_OF_RANGE' });
assert.throws(() => http2.createSecureServer({ connectionWindowSize }),
{ code: 'ERR_OUT_OF_RANGE' });
assert.throws(() => http2.connect('http://localhost:80', { connectionWindowSize }),
{ code: 'ERR_OUT_OF_RANGE' });
}
// Windows below the peer's fixed initial 65535 are permitted: they cannot
// shrink that first burst, but they do throttle everything after it.
for (const connectionWindowSize of [1, 1024, 65534]) {
http2.createServer({ connectionWindowSize }).close();
}
for (const connectionWindowSize of ['1024', null, {}]) {
assert.throws(() => http2.createServer({ connectionWindowSize }),
{ code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => http2.connect('http://localhost:80', { connectionWindowSize }),
{ code: 'ERR_INVALID_ARG_TYPE' });
}
@@ -26,7 +26,8 @@ const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
const IDX_OPTIONS_FLAGS = 13;
const IDX_OPTIONS_CONNECTION_WINDOW_SIZE = 13;
const IDX_OPTIONS_FLAGS = 14;
{
updateOptionsBuffer({
@@ -42,7 +43,8 @@ const IDX_OPTIONS_FLAGS = 13;
maxSettings: 10,
streamResetRate: 11,
streamResetBurst: 12,
strictFieldWhitespaceValidation: false
strictFieldWhitespaceValidation: false,
connectionWindowSize: 13
});
assert.strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -58,6 +60,7 @@ const IDX_OPTIONS_FLAGS = 13;
assert.strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_RATE], 11);
assert.strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST], 12);
assert.strictEqual(optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION], 1);
assert.strictEqual(optionsBuffer[IDX_OPTIONS_CONNECTION_WINDOW_SIZE], 13);
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
@@ -73,11 +76,13 @@ const IDX_OPTIONS_FLAGS = 13;
assert.ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_RATE));
assert.ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_BURST));
assert.ok(flags & (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION));
assert.ok(flags & (1 << IDX_OPTIONS_CONNECTION_WINDOW_SIZE));
}
{
optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] = 0;
optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] = 0;
optionsBuffer[IDX_OPTIONS_CONNECTION_WINDOW_SIZE] = 0;
updateOptionsBuffer({
maxDeflateDynamicTableSize: 1,
@@ -94,6 +99,7 @@ const IDX_OPTIONS_FLAGS = 13;
assert.strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6);
assert.strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH], 0);
assert.strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 0);
assert.strictEqual(optionsBuffer[IDX_OPTIONS_CONNECTION_WINDOW_SIZE], 0);
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
@@ -105,4 +111,5 @@ const IDX_OPTIONS_FLAGS = 13;
assert.ok(!(flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)));
assert.ok(!(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)));
assert.ok(!(flags & (1 << IDX_OPTIONS_CONNECTION_WINDOW_SIZE)));
}