From 2dbd8aad7f22fe014173d63715a3a232fde6d32f Mon Sep 17 00:00:00 2001 From: zhangyongsheng <zhangyongsheng@youzan.com> Date: Tue, 10 Nov 2020 22:22:35 +0800 Subject: [PATCH] http2: allow setting the local window size of a session --- doc/api/errors.md | 5 + doc/api/http2.md | 23 ++++ lib/internal/errors.js | 1 + lib/internal/http2/core.js | 29 ++++- src/node_http2.cc | 21 +++ src/node_http2.h | 3 + test/parallel/test-http2-client-destroy.js | 2 + .../test-http2-client-setLocalWindowSize.js | 121 ++++++++++++++++++ .../test-http2-server-setLocalWindowSize.js | 37 ++++++ 9 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-http2-client-setLocalWindowSize.js create mode 100644 test/parallel/test-http2-server-setLocalWindowSize.js diff --git a/doc/api/errors.md b/doc/api/errors.md index 192c4ecf5772f2..9433e00897877b 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -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` diff --git a/doc/api/http2.md b/doc/api/http2.md index b4977397f350c1..d09d093f3a71d6 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -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 diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 20a9e3e7df3e8a..6eee2d9ce5fe87 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -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); diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index b585d61cd1db92..a9c35d7cef1a6b 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -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, - validateNumber, - validateString, - validateUint32, - isUint32, +const { + isUint32, + validateInt32, + validateInteger, + validateNumber, + validateString, + validateUint32, } = 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. diff --git a/src/node_http2.cc b/src/node_http2.cc index b8e462419e5feb..776393de8f800a 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -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); diff --git a/src/node_http2.h b/src/node_http2.h index 306f5460691e06..9c7ffce2c41955 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -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) diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js index 12da903c6535a0..c91e88079db6bb 100644 --- a/test/parallel/test-http2-client-destroy.js +++ b/test/parallel/test-http2-client-destroy.js @@ -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); diff --git a/test/parallel/test-http2-client-setLocalWindowSize.js b/test/parallel/test-http2-client-setLocalWindowSize.js new file mode 100644 index 00000000000000..8e3b57ed0c1a6b --- /dev/null +++ b/test/parallel/test-http2-client-setLocalWindowSize.js @@ -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(); + })); + })); +} diff --git a/test/parallel/test-http2-server-setLocalWindowSize.js b/test/parallel/test-http2-server-setLocalWindowSize.js new file mode 100644 index 00000000000000..8fcb9b9d0d81b0 --- /dev/null +++ b/test/parallel/test-http2-server-setLocalWindowSize.js @@ -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(); + })); +}));