http2: allow setting the local window size of a session

PR-URL: https://github.com/nodejs/node/pull/35978
Fixes: https://github.com/nodejs/node/issues/31084
Refs: https://github.com/nodejs/node/pull/26962
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ricky Zhou <0x19951125@gmail.com>
This commit is contained in:
zhangyongsheng 2020-11-10 22:22:35 +08:00 committed by Shelley Vohr
parent 0b40568afe
commit 9c6be3cc90
No known key found for this signature in database
GPG Key ID: F13993A75599653C
9 changed files with 237 additions and 5 deletions

View File

@ -1226,6 +1226,11 @@ reached.
An attempt was made to initiate a new push stream from within a push stream.
Nested push streams are not permitted.
<a id="ERR_HTTP2_NO_MEM"></a>
### `ERR_HTTP2_NO_MEM`
Out of memory when using the `http2session.setLocalWindowSize(windowSize)` API.
<a id="ERR_HTTP2_NO_SOCKET_MANIPULATION"></a>
### `ERR_HTTP2_NO_SOCKET_MANIPULATION`

View File

@ -519,6 +519,29 @@ added: v8.4.0
A prototype-less object describing the current remote settings of this
`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
#### `http2session.setLocalWindowSize(windowSize)`
<!-- YAML
added: REPLACEME
-->
* `windowSize` {number}
Sets the local endpoint's window size.
The `windowSize` is the total window size to set, not
the delta.
```js
const http2 = require('http2');
const server = http2.createServer();
const expectedWindowSize = 2 ** 20;
server.on('connect', (session) => {
// Set local window size to be 2 ** 20
session.setLocalWindowSize(expectedWindowSize);
});
```
#### `http2session.setTimeout(msecs, callback)`
<!-- YAML
added: v8.4.0

View File

@ -904,6 +904,7 @@ E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
'Maximum number of pending settings acknowledgements', Error);
E('ERR_HTTP2_NESTED_PUSH',
'A push stream cannot initiate another push stream.', Error);
E('ERR_HTTP2_NO_MEM', 'Out of memory', Error);
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)',
Error);

View File

@ -70,6 +70,7 @@ const {
ERR_HTTP2_INVALID_STREAM,
ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
ERR_HTTP2_NESTED_PUSH,
ERR_HTTP2_NO_MEM,
ERR_HTTP2_NO_SOCKET_MANIPULATION,
ERR_HTTP2_ORIGIN_LENGTH,
ERR_HTTP2_OUT_OF_STREAMS,
@ -101,11 +102,13 @@ const {
},
hideStackFrames
} = require('internal/errors');
const { validateInteger,
const {
isUint32,
validateInt32,
validateInteger,
validateNumber,
validateString,
validateUint32,
isUint32,
} = require('internal/validators');
const fsPromisesInternal = require('internal/fs/promises');
const { utcDate } = require('internal/http');
@ -252,6 +255,7 @@ const {
NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
NGHTTP2_ERR_INVALID_ARGUMENT,
NGHTTP2_ERR_STREAM_CLOSED,
NGHTTP2_ERR_NOMEM,
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_DATE,
@ -1254,6 +1258,21 @@ class Http2Session extends EventEmitter {
this[kHandle].setNextStreamID(id);
}
// Sets the local window size (local endpoints's window size)
// Returns 0 if sucess or throw an exception if NGHTTP2_ERR_NOMEM
// if the window allocation fails
setLocalWindowSize(windowSize) {
if (this.destroyed)
throw new ERR_HTTP2_INVALID_SESSION();
validateInt32(windowSize, 'windowSize', 0);
const ret = this[kHandle].setLocalWindowSize(windowSize);
if (ret === NGHTTP2_ERR_NOMEM) {
this.destroy(new ERR_HTTP2_NO_MEM());
}
}
// If ping is called while we are still connecting, or after close() has
// been called, the ping callback will be invoked immediately will a ping
// cancelled error and a duration of 0.0.

View File

@ -2416,6 +2416,25 @@ void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
Debug(session, "set next stream id to %d", id);
}
// Set local window size (local endpoints's window size) to the given
// window_size for the stream denoted by 0.
// This function returns 0 if it succeeds, or one of a negative codes
void Http2Session::SetLocalWindowSize(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
int32_t window_size = args[0]->Int32Value(env->context()).ToChecked();
int result = nghttp2_session_set_local_window_size(
session->session(), NGHTTP2_FLAG_NONE, 0, window_size);
args.GetReturnValue().Set(result);
Debug(session, "set local window size to %d", window_size);
}
// A TypedArray instance is shared between C++ and JS land to contain the
// SETTINGS (either remote or local). RefreshSettings updates the current
// values established for each of the settings so those can be read in JS land.
@ -3088,6 +3107,8 @@ void Initialize(Local<Object> target,
env->SetProtoMethod(session, "request", Http2Session::Request);
env->SetProtoMethod(session, "setNextStreamID",
Http2Session::SetNextStreamID);
env->SetProtoMethod(session, "setLocalWindowSize",
Http2Session::SetLocalWindowSize);
env->SetProtoMethod(session, "updateChunksSent",
Http2Session::UpdateChunksSent);
env->SetProtoMethod(session, "refreshState", Http2Session::RefreshState);

View File

@ -699,6 +699,8 @@ class Http2Session : public AsyncWrap,
static void Settings(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Request(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetNextStreamID(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetLocalWindowSize(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void Goaway(const v8::FunctionCallbackInfo<v8::Value>& args);
static void UpdateChunksSent(const v8::FunctionCallbackInfo<v8::Value>& args);
static void RefreshState(const v8::FunctionCallbackInfo<v8::Value>& args);
@ -1115,6 +1117,7 @@ class Origins {
V(NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE) \
V(NGHTTP2_ERR_INVALID_ARGUMENT) \
V(NGHTTP2_ERR_STREAM_CLOSED) \
V(NGHTTP2_ERR_NOMEM) \
V(STREAM_OPTION_EMPTY_PAYLOAD) \
V(STREAM_OPTION_GET_TRAILERS)

View File

@ -77,6 +77,7 @@ const Countdown = require('../common/countdown');
};
assert.throws(() => client.setNextStreamID(), sessionError);
assert.throws(() => client.setLocalWindowSize(), sessionError);
assert.throws(() => client.ping(), sessionError);
assert.throws(() => client.settings({}), sessionError);
assert.throws(() => client.goaway(), sessionError);
@ -87,6 +88,7 @@ const Countdown = require('../common/countdown');
// so that state.destroyed is set to true
setImmediate(() => {
assert.throws(() => client.setNextStreamID(), sessionError);
assert.throws(() => client.setLocalWindowSize(), sessionError);
assert.throws(() => client.ping(), sessionError);
assert.throws(() => client.settings({}), sessionError);
assert.throws(() => client.goaway(), sessionError);

View File

@ -0,0 +1,121 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
{
const server = http2.createServer();
server.on('stream', common.mustNotCall((stream) => {
stream.respond();
stream.end('ok');
}));
const types = {
boolean: true,
function: () => {},
number: 1,
object: {},
array: [],
null: null,
};
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
client.on('connect', common.mustCall(() => {
const outOfRangeNum = 2 ** 32;
assert.throws(
() => client.setLocalWindowSize(outOfRangeNum),
{
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE',
message: 'The value of "windowSize" is out of range.' +
' It must be >= 0 && <= 2147483647. Received ' + outOfRangeNum
}
);
// Throw if something other than number is passed to setLocalWindowSize
Object.entries(types).forEach(([type, value]) => {
if (type === 'number') {
return;
}
assert.throws(
() => client.setLocalWindowSize(value),
{
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "windowSize" argument must be of type number.' +
common.invalidArgTypeHelper(value)
}
);
});
server.close();
client.close();
}));
}));
}
{
const server = http2.createServer();
server.on('stream', common.mustNotCall((stream) => {
stream.respond();
stream.end('ok');
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
client.on('connect', common.mustCall(() => {
const windowSize = 2 ** 20;
const defaultSetting = http2.getDefaultSettings();
client.setLocalWindowSize(windowSize);
assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
assert.strictEqual(client.state.localWindowSize, windowSize);
assert.strictEqual(
client.state.remoteWindowSize,
defaultSetting.initialWindowSize
);
server.close();
client.close();
}));
}));
}
{
const server = http2.createServer();
server.on('stream', common.mustNotCall((stream) => {
stream.respond();
stream.end('ok');
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
client.on('connect', common.mustCall(() => {
const windowSize = 20;
const defaultSetting = http2.getDefaultSettings();
client.setLocalWindowSize(windowSize);
assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
assert.strictEqual(
client.state.localWindowSize,
defaultSetting.initialWindowSize
);
assert.strictEqual(
client.state.remoteWindowSize,
defaultSetting.initialWindowSize
);
server.close();
client.close();
}));
}));
}

View File

@ -0,0 +1,37 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
stream.respond();
stream.end('ok');
}));
server.on('session', common.mustCall((session) => {
const windowSize = 2 ** 20;
const defaultSetting = http2.getDefaultSettings();
session.setLocalWindowSize(windowSize);
assert.strictEqual(session.state.effectiveLocalWindowSize, windowSize);
assert.strictEqual(session.state.localWindowSize, windowSize);
assert.strictEqual(
session.state.remoteWindowSize,
defaultSetting.initialWindowSize
);
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.resume();
req.on('close', common.mustCall(() => {
client.close();
server.close();
}));
}));