Skip to content

Commit a6c6cbb

Browse files
zhangyongshengMylesBorins
zhangyongsheng
authored andcommitted
http2: allow setting the local window size of a session
PR-URL: #35978 Fixes: #31084 Refs: #26962 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Ricky Zhou <[email protected]>
1 parent 68bbebd commit a6c6cbb

9 files changed

+231
-0
lines changed

doc/api/errors.md

+5
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,11 @@ reached.
11371137
An attempt was made to initiate a new push stream from within a push stream.
11381138
Nested push streams are not permitted.
11391139

1140+
<a id="ERR_HTTP2_NO_MEM"></a>
1141+
### `ERR_HTTP2_NO_MEM`
1142+
1143+
Out of memory when using the `http2session.setLocalWindowSize(windowSize)` API.
1144+
11401145
<a id="ERR_HTTP2_NO_SOCKET_MANIPULATION"></a>
11411146
### `ERR_HTTP2_NO_SOCKET_MANIPULATION`
11421147

doc/api/http2.md

+23
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,29 @@ added: v8.4.0
518518
A prototype-less object describing the current remote settings of this
519519
`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
520520

521+
#### `http2session.setLocalWindowSize(windowSize)`
522+
<!-- YAML
523+
added: REPLACEME
524+
-->
525+
526+
* `windowSize` {number}
527+
528+
Sets the local endpoint's window size.
529+
The `windowSize` is the total window size to set, not
530+
the delta.
531+
532+
```js
533+
const http2 = require('http2');
534+
535+
const server = http2.createServer();
536+
const expectedWindowSize = 2 ** 20;
537+
server.on('connect', (session) => {
538+
539+
// Set local window size to be 2 ** 20
540+
session.setLocalWindowSize(expectedWindowSize);
541+
});
542+
```
543+
521544
#### `http2session.setTimeout(msecs, callback)`
522545
<!-- YAML
523546
added: v8.4.0

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,7 @@ E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
917917
'Maximum number of pending settings acknowledgements', Error);
918918
E('ERR_HTTP2_NESTED_PUSH',
919919
'A push stream cannot initiate another push stream.', Error);
920+
E('ERR_HTTP2_NO_MEM', 'Out of memory', Error);
920921
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
921922
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)',
922923
Error);

lib/internal/http2/core.js

+18
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const {
7171
ERR_HTTP2_INVALID_STREAM,
7272
ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
7373
ERR_HTTP2_NESTED_PUSH,
74+
ERR_HTTP2_NO_MEM,
7475
ERR_HTTP2_NO_SOCKET_MANIPULATION,
7576
ERR_HTTP2_ORIGIN_LENGTH,
7677
ERR_HTTP2_OUT_OF_STREAMS,
@@ -104,6 +105,7 @@ const {
104105
} = require('internal/errors');
105106
const {
106107
isUint32,
108+
validateInt32,
107109
validateNumber,
108110
validateString,
109111
validateUint32,
@@ -252,6 +254,7 @@ const {
252254
NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
253255
NGHTTP2_ERR_INVALID_ARGUMENT,
254256
NGHTTP2_ERR_STREAM_CLOSED,
257+
NGHTTP2_ERR_NOMEM,
255258

256259
HTTP2_HEADER_AUTHORITY,
257260
HTTP2_HEADER_DATE,
@@ -1290,6 +1293,21 @@ class Http2Session extends EventEmitter {
12901293
this[kHandle].setNextStreamID(id);
12911294
}
12921295

1296+
// Sets the local window size (local endpoints's window size)
1297+
// Returns 0 if sucess or throw an exception if NGHTTP2_ERR_NOMEM
1298+
// if the window allocation fails
1299+
setLocalWindowSize(windowSize) {
1300+
if (this.destroyed)
1301+
throw new ERR_HTTP2_INVALID_SESSION();
1302+
1303+
validateInt32(windowSize, 'windowSize', 0);
1304+
const ret = this[kHandle].setLocalWindowSize(windowSize);
1305+
1306+
if (ret === NGHTTP2_ERR_NOMEM) {
1307+
this.destroy(new ERR_HTTP2_NO_MEM());
1308+
}
1309+
}
1310+
12931311
// If ping is called while we are still connecting, or after close() has
12941312
// been called, the ping callback will be invoked immediately with a ping
12951313
// cancelled error and a duration of 0.0.

src/node_http2.cc

+21
Original file line numberDiff line numberDiff line change
@@ -2433,6 +2433,25 @@ void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
24332433
Debug(session, "set next stream id to %d", id);
24342434
}
24352435

2436+
// Set local window size (local endpoints's window size) to the given
2437+
// window_size for the stream denoted by 0.
2438+
// This function returns 0 if it succeeds, or one of a negative codes
2439+
void Http2Session::SetLocalWindowSize(
2440+
const FunctionCallbackInfo<Value>& args) {
2441+
Environment* env = Environment::GetCurrent(args);
2442+
Http2Session* session;
2443+
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
2444+
2445+
int32_t window_size = args[0]->Int32Value(env->context()).ToChecked();
2446+
2447+
int result = nghttp2_session_set_local_window_size(
2448+
session->session(), NGHTTP2_FLAG_NONE, 0, window_size);
2449+
2450+
args.GetReturnValue().Set(result);
2451+
2452+
Debug(session, "set local window size to %d", window_size);
2453+
}
2454+
24362455
// A TypedArray instance is shared between C++ and JS land to contain the
24372456
// SETTINGS (either remote or local). RefreshSettings updates the current
24382457
// values established for each of the settings so those can be read in JS land.
@@ -3105,6 +3124,8 @@ void Initialize(Local<Object> target,
31053124
env->SetProtoMethod(session, "request", Http2Session::Request);
31063125
env->SetProtoMethod(session, "setNextStreamID",
31073126
Http2Session::SetNextStreamID);
3127+
env->SetProtoMethod(session, "setLocalWindowSize",
3128+
Http2Session::SetLocalWindowSize);
31083129
env->SetProtoMethod(session, "updateChunksSent",
31093130
Http2Session::UpdateChunksSent);
31103131
env->SetProtoMethod(session, "refreshState", Http2Session::RefreshState);

src/node_http2.h

+3
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,8 @@ class Http2Session : public AsyncWrap,
700700
static void Settings(const v8::FunctionCallbackInfo<v8::Value>& args);
701701
static void Request(const v8::FunctionCallbackInfo<v8::Value>& args);
702702
static void SetNextStreamID(const v8::FunctionCallbackInfo<v8::Value>& args);
703+
static void SetLocalWindowSize(
704+
const v8::FunctionCallbackInfo<v8::Value>& args);
703705
static void Goaway(const v8::FunctionCallbackInfo<v8::Value>& args);
704706
static void UpdateChunksSent(const v8::FunctionCallbackInfo<v8::Value>& args);
705707
static void RefreshState(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -1116,6 +1118,7 @@ class Origins {
11161118
V(NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE) \
11171119
V(NGHTTP2_ERR_INVALID_ARGUMENT) \
11181120
V(NGHTTP2_ERR_STREAM_CLOSED) \
1121+
V(NGHTTP2_ERR_NOMEM) \
11191122
V(STREAM_OPTION_EMPTY_PAYLOAD) \
11201123
V(STREAM_OPTION_GET_TRAILERS)
11211124

test/parallel/test-http2-client-destroy.js

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const Countdown = require('../common/countdown');
7878
};
7979

8080
assert.throws(() => client.setNextStreamID(), sessionError);
81+
assert.throws(() => client.setLocalWindowSize(), sessionError);
8182
assert.throws(() => client.ping(), sessionError);
8283
assert.throws(() => client.settings({}), sessionError);
8384
assert.throws(() => client.goaway(), sessionError);
@@ -88,6 +89,7 @@ const Countdown = require('../common/countdown');
8889
// so that state.destroyed is set to true
8990
setImmediate(() => {
9091
assert.throws(() => client.setNextStreamID(), sessionError);
92+
assert.throws(() => client.setLocalWindowSize(), sessionError);
9193
assert.throws(() => client.ping(), sessionError);
9294
assert.throws(() => client.settings({}), sessionError);
9395
assert.throws(() => client.goaway(), sessionError);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const http2 = require('http2');
9+
10+
{
11+
const server = http2.createServer();
12+
server.on('stream', common.mustNotCall((stream) => {
13+
stream.respond();
14+
stream.end('ok');
15+
}));
16+
17+
const types = {
18+
boolean: true,
19+
function: () => {},
20+
number: 1,
21+
object: {},
22+
array: [],
23+
null: null,
24+
};
25+
26+
server.listen(0, common.mustCall(() => {
27+
const client = http2.connect(`http://localhost:${server.address().port}`);
28+
29+
client.on('connect', common.mustCall(() => {
30+
const outOfRangeNum = 2 ** 32;
31+
assert.throws(
32+
() => client.setLocalWindowSize(outOfRangeNum),
33+
{
34+
name: 'RangeError',
35+
code: 'ERR_OUT_OF_RANGE',
36+
message: 'The value of "windowSize" is out of range.' +
37+
' It must be >= 0 && <= 2147483647. Received ' + outOfRangeNum
38+
}
39+
);
40+
41+
// Throw if something other than number is passed to setLocalWindowSize
42+
Object.entries(types).forEach(([type, value]) => {
43+
if (type === 'number') {
44+
return;
45+
}
46+
47+
assert.throws(
48+
() => client.setLocalWindowSize(value),
49+
{
50+
name: 'TypeError',
51+
code: 'ERR_INVALID_ARG_TYPE',
52+
message: 'The "windowSize" argument must be of type number.' +
53+
common.invalidArgTypeHelper(value)
54+
}
55+
);
56+
});
57+
58+
server.close();
59+
client.close();
60+
}));
61+
}));
62+
}
63+
64+
{
65+
const server = http2.createServer();
66+
server.on('stream', common.mustNotCall((stream) => {
67+
stream.respond();
68+
stream.end('ok');
69+
}));
70+
71+
server.listen(0, common.mustCall(() => {
72+
const client = http2.connect(`http://localhost:${server.address().port}`);
73+
74+
client.on('connect', common.mustCall(() => {
75+
const windowSize = 2 ** 20;
76+
const defaultSetting = http2.getDefaultSettings();
77+
client.setLocalWindowSize(windowSize);
78+
79+
assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
80+
assert.strictEqual(client.state.localWindowSize, windowSize);
81+
assert.strictEqual(
82+
client.state.remoteWindowSize,
83+
defaultSetting.initialWindowSize
84+
);
85+
86+
server.close();
87+
client.close();
88+
}));
89+
}));
90+
}
91+
92+
{
93+
const server = http2.createServer();
94+
server.on('stream', common.mustNotCall((stream) => {
95+
stream.respond();
96+
stream.end('ok');
97+
}));
98+
99+
server.listen(0, common.mustCall(() => {
100+
const client = http2.connect(`http://localhost:${server.address().port}`);
101+
102+
client.on('connect', common.mustCall(() => {
103+
const windowSize = 20;
104+
const defaultSetting = http2.getDefaultSettings();
105+
client.setLocalWindowSize(windowSize);
106+
107+
assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
108+
assert.strictEqual(
109+
client.state.localWindowSize,
110+
defaultSetting.initialWindowSize
111+
);
112+
assert.strictEqual(
113+
client.state.remoteWindowSize,
114+
defaultSetting.initialWindowSize
115+
);
116+
117+
server.close();
118+
client.close();
119+
}));
120+
}));
121+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const http2 = require('http2');
9+
10+
const server = http2.createServer();
11+
server.on('stream', common.mustCall((stream) => {
12+
stream.respond();
13+
stream.end('ok');
14+
}));
15+
server.on('session', common.mustCall((session) => {
16+
const windowSize = 2 ** 20;
17+
const defaultSetting = http2.getDefaultSettings();
18+
session.setLocalWindowSize(windowSize);
19+
20+
assert.strictEqual(session.state.effectiveLocalWindowSize, windowSize);
21+
assert.strictEqual(session.state.localWindowSize, windowSize);
22+
assert.strictEqual(
23+
session.state.remoteWindowSize,
24+
defaultSetting.initialWindowSize
25+
);
26+
}));
27+
28+
server.listen(0, common.mustCall(() => {
29+
const client = http2.connect(`http://localhost:${server.address().port}`);
30+
31+
const req = client.request();
32+
req.resume();
33+
req.on('close', common.mustCall(() => {
34+
client.close();
35+
server.close();
36+
}));
37+
}));

0 commit comments

Comments
 (0)