summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames M Snell <jasnell@gmail.com>2017-11-15 10:55:31 -0800
committerAnna Henningsen <anna@addaleax.net>2017-11-21 12:47:54 +0100
commit69e6c5a212622ec15b8c2cf904480b6582c6c3a5 (patch)
treea6368dc8e37112d82ceb8b4dff6120b4bf027429
parent2ba93f6ed7c96684ed7aeaa32db59ae8e5885f03 (diff)
downloadandroid-node-v8-69e6c5a212622ec15b8c2cf904480b6582c6c3a5.tar.gz
android-node-v8-69e6c5a212622ec15b8c2cf904480b6582c6c3a5.tar.bz2
android-node-v8-69e6c5a212622ec15b8c2cf904480b6582c6c3a5.zip
http2: major update to internals
This update does several significant things: 1. It eliminates the base Nghttp2* classes and folds those in to node::http2::Http2Session and node::http2::Http2Stream 2. It makes node::http2::Http2Stream a StreamBase instance and sends that out to JS-land to act as the [kHandle] for the JavaScript Http2Stream class. 3. It shifts some of the callbacks from C++ off of the JavaScript Http2Session class to the Http2Stream class. 4. It refactors the data provider structure for FD and Stream based sending to help encapsulate those functions easier 5. It streamlines some of the functions at the C++ layer to eliminate now unnecessary redirections 6. It cleans up node_http2.cc for better readability and maintainability 7. It refactors some of the debug output 8. Because Http2Stream instances are now StreamBases, they are now also trackable using async-hooks 9. The Stream::OnRead algorithm has been simplified with a couple bugs fixed. 10. I've eliminated node_http2_core.h and node_http2_core-inl.h 11. Detect invalid handshake a report protocol error to session 12. Refactor out of memory error, improve other errors 13. Add Http2Session.prototype.ping PR-URL: https://github.com/nodejs/node/pull/17105 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Sebastiaan Deckers <sebdeckers83@gmail.com>
-rw-r--r--doc/api/errors.md10
-rw-r--r--doc/api/http2.md94
-rw-r--r--lib/internal/errors.js2
-rw-r--r--lib/internal/http2/core.js741
-rw-r--r--lib/internal/http2/util.js104
-rw-r--r--node.gyp2
-rw-r--r--src/async_wrap.h3
-rw-r--r--src/env.h2
-rw-r--r--src/node_http2.cc2393
-rw-r--r--src/node_http2.h671
-rw-r--r--src/node_http2_core-inl.h925
-rw-r--r--src/node_http2_core.h516
-rwxr-xr-xsrc/node_http2_state.h1
-rw-r--r--test/parallel/test-http2-client-data-end.js2
-rw-r--r--test/parallel/test-http2-client-destroy.js3
-rw-r--r--test/parallel/test-http2-client-http1-server.js27
-rw-r--r--test/parallel/test-http2-client-onconnect-errors.js28
-rw-r--r--test/parallel/test-http2-client-priority-before-connect.js2
-rw-r--r--test/parallel/test-http2-client-rststream-before-connect.js10
-rw-r--r--test/parallel/test-http2-client-settings-errors.js84
-rw-r--r--test/parallel/test-http2-compat-serverrequest-trailers.js2
-rw-r--r--test/parallel/test-http2-getpackedsettings.js3
-rw-r--r--test/parallel/test-http2-info-headers-errors.js22
-rw-r--r--test/parallel/test-http2-invalidargtypes-errors.js10
-rw-r--r--test/parallel/test-http2-no-more-streams.js53
-rw-r--r--test/parallel/test-http2-ping.js87
-rw-r--r--test/parallel/test-http2-pipe.js16
-rw-r--r--test/parallel/test-http2-priority-errors.js109
-rw-r--r--test/parallel/test-http2-respond-errors.js21
-rw-r--r--test/parallel/test-http2-respond-with-fd-errors.js24
-rw-r--r--test/parallel/test-http2-rststream-errors.js22
-rw-r--r--test/parallel/test-http2-server-http1-client.js22
-rw-r--r--test/parallel/test-http2-server-push-stream-errors.js17
-rw-r--r--test/parallel/test-http2-server-stream-session-destroy.js6
-rw-r--r--test/parallel/test-http2-shutdown-errors.js2
-rw-r--r--test/parallel/test-http2-util-update-options-buffer.js16
-rw-r--r--test/sequential/test-async-wrap-getasyncid.js3
37 files changed, 2874 insertions, 3181 deletions
diff --git a/doc/api/errors.md b/doc/api/errors.md
index c2a1a4f214..d7b5de448f 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -908,6 +908,16 @@ limit.
A message payload was specified for an HTTP response code for which a payload is
forbidden.
+<a id="ERR_HTTP2_PING_CANCEL"></a>
+### ERR_HTTP2_PING_CANCEL
+
+An HTTP/2 ping was cancelled.
+
+<a id="ERR_HTTP2_PING_LENGTH"></a>
+### ERR_HTTP2_PING_LENGTH
+
+HTTP/2 ping payloads must be exactly 8 bytes in length.
+
<a id="ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED"></a>
### ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED
diff --git a/doc/api/http2.md b/doc/api/http2.md
index c1000661a9..3500c563a2 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -344,6 +344,44 @@ acknowledgement for a sent SETTINGS frame. Will be `true` after calling the
`http2session.settings()` method. Will be `false` once all sent SETTINGS
frames have been acknowledged.
+#### http2session.ping([payload, ]callback)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `payload` {Buffer|TypedArray|DataView} Optional ping payload.
+* `callback` {Function}
+* Returns: {boolean}
+
+Sends a `PING` frame to the connected HTTP/2 peer. A `callback` function must
+be provided. The method will return `true` if the `PING` was sent, `false`
+otherwise.
+
+The maximum number of outstanding (unacknowledged) pings is determined by the
+`maxOutstandingPings` configuration option. The default maximum is 10.
+
+If provided, the `payload` must be a `Buffer`, `TypedArray`, or `DataView`
+containing 8 bytes of data that will be transmitted with the `PING` and
+returned with the ping acknowledgement.
+
+The callback will be invoked with three arguments: an error argument that will
+be `null` if the `PING` was successfully acknowledged, a `duration` argument
+that reports the number of milliseconds elapsed since the ping was sent and the
+acknowledgement was received, and a `Buffer` containing the 8-byte `PING`
+payload.
+
+```js
+session.ping(Buffer.from('abcdefgh'), (err, duration, payload) => {
+ if (!err) {
+ console.log(`Ping acknowledged in ${duration} milliseconds`);
+ console.log(`With payload '${payload.toString()}`);
+ }
+});
+```
+
+If the `payload` argument is not specified, the default payload will be the
+64-bit timestamp (little endian) marking the start of the `PING` duration.
+
#### http2session.remoteSettings
<!-- YAML
added: v8.4.0
@@ -411,19 +449,6 @@ the trailing header fields to send to the peer.
will be emitted if the `getTrailers` callback attempts to set such header
fields.
-#### http2session.rstStream(stream, code)
-<!-- YAML
-added: v8.4.0
--->
-
-* stream {Http2Stream}
-* code {number} Unsigned 32-bit integer identifying the error code. **Default:**
- `http2.constant.NGHTTP2_NO_ERROR` (`0x00`)
-* Returns: {undefined}
-
-Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing the given
-`Http2Stream` to be closed on both sides using [error code][] `code`.
-
#### http2session.setTimeout(msecs, callback)
<!-- YAML
added: v8.4.0
@@ -515,28 +540,6 @@ added: v8.4.0
An object describing the current status of this `Http2Session`.
-#### http2session.priority(stream, options)
-<!-- YAML
-added: v8.4.0
--->
-
-* `stream` {Http2Stream}
-* `options` {Object}
- * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
- the given stream is made the sole direct dependency of the parent, with
- all other existing dependents made a dependent of the given stream. **Default:**
- `false`
- * `parent` {number} Specifies the numeric identifier of a stream the given
- stream is dependent on.
- * `weight` {number} Specifies the relative dependency of a stream in relation
- to other streams with the same `parent`. The value is a number between `1`
- and `256` (inclusive).
- * `silent` {boolean} When `true`, changes the priority locally without
- sending a `PRIORITY` frame to the connected peer.
-* Returns: {undefined}
-
-Updates the priority for the given `Http2Stream` instance.
-
#### http2session.settings(settings)
<!-- YAML
added: v8.4.0
@@ -624,8 +627,7 @@ is not yet ready for use.
All [`Http2Stream`][] instances are destroyed either when:
* An `RST_STREAM` frame for the stream is received by the connected peer.
-* The `http2stream.rstStream()` or `http2session.rstStream()` methods are
- called.
+* The `http2stream.rstStream()` methods is called.
* The `http2stream.destroy()` or `http2session.destroy()` methods are called.
When an `Http2Stream` instance is destroyed, an attempt will be made to send an
@@ -1473,6 +1475,10 @@ not be emitted.
<!-- YAML
added: v8.4.0
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/17105
+ description: Added the `maxOutstandingPings` option with a default limit of
+ 10.
- version: v9.1.0
pr-url: https://github.com/nodejs/node/pull/16676
description: Added the `maxHeaderListPairs` option with a default limit of
@@ -1484,6 +1490,8 @@ changes:
for deflating header fields. **Default:** `4Kib`
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
**Default:** `128`. The minimum value is `4`.
+ * `maxOutstandingPings` {number} Sets the maximum number of outstanding,
+ unacknowledged pings. The default is `10`.
* `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a
serialized, compressed block of headers. Attempts to send headers that
exceed this limit will result in a `'frameError'` event being emitted
@@ -1535,6 +1543,10 @@ server.listen(80);
<!-- YAML
added: v8.4.0
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/17105
+ description: Added the `maxOutstandingPings` option with a default limit of
+ 10.
- version: v9.1.0
pr-url: https://github.com/nodejs/node/pull/16676
description: Added the `maxHeaderListPairs` option with a default limit of
@@ -1549,6 +1561,8 @@ changes:
for deflating header fields. **Default:** `4Kib`
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
**Default:** `128`. The minimum value is `4`.
+ * `maxOutstandingPings` {number} Sets the maximum number of outstanding,
+ unacknowledged pings. The default is `10`.
* `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a
serialized, compressed block of headers. Attempts to send headers that
exceed this limit will result in a `'frameError'` event being emitted
@@ -1607,6 +1621,10 @@ server.listen(80);
<!-- YAML
added: v8.4.0
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/17105
+ description: Added the `maxOutstandingPings` option with a default limit of
+ 10.
- version: v9.1.0
pr-url: https://github.com/nodejs/node/pull/16676
description: Added the `maxHeaderListPairs` option with a default limit of
@@ -1619,6 +1637,8 @@ changes:
for deflating header fields. **Default:** `4Kib`
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
**Default:** `128`. The minimum value is `1`.
+ * `maxOutstandingPings` {number} Sets the maximum number of outstanding,
+ unacknowledged pings. The default is `10`.
* `maxReservedRemoteStreams` {number} Sets the maximum number of reserved push
streams the client will accept at any given time. Once the current number of
currently reserved push streams exceeds reaches this limit, new push streams
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 4da05899b0..3be2705648 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -299,6 +299,8 @@ E('ERR_HTTP2_OUT_OF_STREAMS',
'No stream ID is available because maximum stream ID has been reached');
E('ERR_HTTP2_PAYLOAD_FORBIDDEN',
'Responses with %s status must not have a payload');
+E('ERR_HTTP2_PING_CANCEL', 'HTTP2 ping cancelled');
+E('ERR_HTTP2_PING_LENGTH', 'HTTP2 ping payload must be 8 bytes');
E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers');
E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams');
E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent');
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 2e20c9ea88..de20eb23f2 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -4,6 +4,7 @@
require('internal/util').assertCrypto();
+const { async_id_symbol } = process.binding('async_wrap');
const binding = process.binding('http2');
const assert = require('assert');
const { Buffer } = require('buffer');
@@ -22,11 +23,12 @@ const { onServerStream,
} = require('internal/http2/compat');
const { utcDate } = require('internal/http');
const { promisify } = require('internal/util');
-const { isUint8Array } = require('internal/util/types');
+const { isArrayBufferView } = require('internal/util/types');
const { _connectionListener: httpConnectionListener } = require('http');
const { createPromise, promiseResolve } = process.binding('util');
const debug = util.debuglog('http2');
+const kMaxStreams = (2 ** 31) - 1;
const {
assertIsObject,
@@ -53,7 +55,7 @@ const {
unenroll
} = require('timers');
-const { WriteWrap } = process.binding('stream_wrap');
+const { ShutdownWrap, WriteWrap } = process.binding('stream_wrap');
const { constants } = binding;
const NETServer = net.Server;
@@ -100,7 +102,6 @@ const {
NGHTTP2_REFUSED_STREAM,
NGHTTP2_SESSION_CLIENT,
NGHTTP2_SESSION_SERVER,
- NGHTTP2_ERR_NOMEM,
NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
NGHTTP2_ERR_INVALID_ARGUMENT,
NGHTTP2_ERR_STREAM_CLOSED,
@@ -145,7 +146,7 @@ function emit(self, ...args) {
// create the associated Http2Stream instance and emit the 'stream'
// event. If the stream is not new, emit the 'headers' event to pass
// the block of headers on.
-function onSessionHeaders(id, cat, flags, headers) {
+function onSessionHeaders(handle, id, cat, flags, headers) {
const owner = this[kOwner];
const type = owner[kType];
_unrefActive(owner);
@@ -160,11 +161,10 @@ function onSessionHeaders(id, cat, flags, headers) {
const obj = toHeaderObject(headers);
if (stream === undefined) {
+ const opts = { readable: !endOfStream };
// owner[kType] can be only one of two possible values
if (type === NGHTTP2_SESSION_SERVER) {
- stream = new ServerHttp2Stream(owner, id,
- { readable: !endOfStream },
- obj);
+ stream = new ServerHttp2Stream(owner, handle, id, opts, obj);
if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
// For head requests, there must not be a body...
// end the writable side immediately.
@@ -172,7 +172,7 @@ function onSessionHeaders(id, cat, flags, headers) {
stream[kState].headRequest = true;
}
} else {
- stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream });
+ stream = new ClientHttp2Stream(owner, handle, id, opts);
}
streams.set(id, stream);
process.nextTick(emit, owner, 'stream', stream, obj, flags, headers);
@@ -210,16 +210,8 @@ function onSessionHeaders(id, cat, flags, headers) {
// event handler returns, those are sent off for processing. Note that this
// is a necessarily synchronous operation. We need to know immediately if
// there are trailing headers to send.
-function onSessionTrailers(id) {
- const owner = this[kOwner];
- debug(`[${sessionName(owner[kType])}] checking for trailers`);
- const streams = owner[kState].streams;
- const stream = streams.get(id);
- // It should not be possible for the stream not to exist at this point.
- // If it does not exist, there is something very very wrong.
- assert(stream !== undefined,
- 'Internal HTTP/2 Failure. Stream does not exist. Please ' +
- 'report this as a bug in Node.js');
+function onStreamTrailers() {
+ const stream = this[kOwner];
const trailers = Object.create(null);
stream[kState].getTrailers.call(stream, trailers);
const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
@@ -234,24 +226,16 @@ function onSessionTrailers(id) {
// Http2Stream instance. Note that this event is distinctly different than the
// require('stream') interface 'close' event which deals with the state of the
// Readable and Writable sides of the Duplex.
-function onSessionStreamClose(id, code) {
- const owner = this[kOwner];
- debug(`[${sessionName(owner[kType])}] session is closing the stream ` +
- `${id}: ${code}`);
- const stream = owner[kState].streams.get(id);
- if (stream === undefined)
- return;
- _unrefActive(owner);
- // Set the rst state for the stream
+function onStreamClose(code) {
+ const stream = this[kOwner];
+ _unrefActive(stream);
+ _unrefActive(stream[kSession]);
abort(stream);
const state = stream[kState];
state.rst = true;
state.rstCode = code;
-
- if (state.fd !== undefined) {
- debug(`Closing fd ${state.fd} for stream ${id}`);
+ if (state.fd !== undefined)
fs.close(state.fd, afterFDClose.bind(stream));
- }
setImmediate(stream.destroy.bind(stream));
}
@@ -270,26 +254,16 @@ function onSessionError(error) {
// Receives a chunk of data for a given stream and forwards it on
// to the Http2Stream Duplex for processing.
-function onSessionRead(nread, buf, handle) {
- const owner = this[kOwner];
- const streams = owner[kState].streams;
- const id = handle.id;
- const stream = streams.get(id);
- // It should not be possible for the stream to not exist at this point.
- // If it does not, something is very very wrong
- assert(stream !== undefined,
- 'Internal HTTP/2 Failure. Stream does not exist. Please ' +
- 'report this as a bug in Node.js');
- _unrefActive(owner); // Reset the session timeout timer
- _unrefActive(stream); // Reset the stream timeout timer
+function onStreamRead(nread, buf, handle) {
+ const stream = handle[kOwner];
+ _unrefActive(stream);
+ _unrefActive(stream[kSession]);
if (nread >= 0 && !stream.destroyed) {
- // prevent overflowing the buffer while pause figures out the
- // stream needs to actually pause and streamOnPause runs
- if (!stream.push(buf))
- owner[kHandle].streamReadStop(id);
+ if (!stream.push(buf)) {
+ handle.readStop();
+ }
return;
}
-
// Last chunk was received. End the readable side.
stream.push(null);
}
@@ -310,11 +284,8 @@ function onSettings(ack) {
owner[kRemoteSettings] = undefined;
}
// Only emit the event if there are listeners registered
- if (owner.listenerCount(event) > 0) {
- const settings = event === 'localSettings' ?
- owner.localSettings : owner.remoteSettings;
- process.nextTick(emit, owner, event, settings);
- }
+ if (owner.listenerCount(event) > 0)
+ process.nextTick(emit, owner, event, owner[event]);
}
// If the stream exists, an attempt will be made to emit an event
@@ -417,15 +388,14 @@ function requestOnConnect(headers, options) {
// ret will be either the reserved stream ID (if positive)
// or an error code (if negative)
- const ret = session[kHandle].submitRequest(headersList,
- streamOptions,
- options.parent | 0,
- options.weight | 0,
- !!options.exclusive);
+ const ret = session[kHandle].request(headersList,
+ streamOptions,
+ options.parent | 0,
+ options.weight | 0,
+ !!options.exclusive);
// In an error condition, one of three possible response codes will be
// possible:
- // * NGHTTP2_ERR_NOMEM - Out of memory, this should be fatal to the process.
// * NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE - Maximum stream ID is reached, this
// is fatal for the session
// * NGHTTP2_ERR_INVALID_ARGUMENT - Stream was made dependent on itself, this
@@ -433,31 +403,27 @@ function requestOnConnect(headers, options) {
// For the first two, emit the error on the session,
// For the third, emit the error on the stream, it will bubble up to the
// session if not handled.
- let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, session, 'error', err);
- break;
- case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
- err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
- process.nextTick(emit, session, 'error', err);
- break;
- case NGHTTP2_ERR_INVALID_ARGUMENT:
- err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY');
- process.nextTick(emit, this, 'error', err);
- break;
- default:
- // Some other, unexpected error was returned. Emit on the session.
- if (ret < 0) {
- err = new NghttpError(ret);
- process.nextTick(emit, session, 'error', err);
+ if (typeof ret === 'number') {
+ let err;
+ let target = session;
+ switch (ret) {
+ case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
+ err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
+ target = this;
break;
- }
- debug(`[${sessionName(session[kType])}] stream ${ret} initialized`);
- this[kInit](ret);
- streams.set(ret, this);
+ case NGHTTP2_ERR_INVALID_ARGUMENT:
+ err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY');
+ target = this;
+ break;
+ default:
+ err = new NghttpError(ret);
+ }
+ process.nextTick(emit, target, 'error', err);
+ return;
}
+ const id = ret.id();
+ streams.set(id, this);
+ this[kInit](id, ret);
}
function validatePriorityOptions(options) {
@@ -502,6 +468,12 @@ function validatePriorityOptions(options) {
}
}
+function onSessionInternalError(code) {
+ const owner = this[kOwner];
+ const err = new NghttpError(code);
+ process.nextTick(emit, owner, 'error', err);
+}
+
// Creates the internal binding.Http2Session handle for an Http2Session
// instance. This occurs only after the socket connection has been
// established. Note: the binding.Http2Session will take over ownership
@@ -514,13 +486,11 @@ function setupHandle(session, socket, type, options) {
updateOptionsBuffer(options);
const handle = new binding.Http2Session(type);
handle[kOwner] = session;
+ handle.error = onSessionInternalError;
handle.onpriority = onPriority;
handle.onsettings = onSettings;
handle.onheaders = onSessionHeaders;
- handle.ontrailers = onSessionTrailers;
- handle.onstreamclose = onSessionStreamClose;
handle.onerror = onSessionError;
- handle.onread = onSessionRead;
handle.onframeerror = onFrameError;
handle.ongoawaydata = onGoawayData;
@@ -548,134 +518,87 @@ function submitSettings(settings) {
debug(`[${sessionName(type)}] submitting actual settings`);
_unrefActive(this);
this[kLocalSettings] = undefined;
-
updateSettingsBuffer(settings);
- const ret = this[kHandle].submitSettings();
- let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, this, 'error', err);
- break;
- default:
- // Some other unexpected error was reported.
- if (ret < 0) {
- err = new NghttpError(ret);
- process.nextTick(emit, this, 'error', err);
- }
- }
+ this[kHandle].settings();
debug(`[${sessionName(type)}] settings complete`);
}
// Submits a PRIORITY frame to be sent to the remote peer
// Note: If the silent option is true, the change will be made
// locally with no PRIORITY frame sent.
-function submitPriority(stream, options) {
- const type = this[kType];
- debug(`[${sessionName(type)}] submitting actual priority`);
+function submitPriority(options) {
_unrefActive(this);
+ _unrefActive(this[kSession]);
- const ret = this[kHandle].submitPriority(stream[kID],
- options.parent | 0,
- options.weight | 0,
- !!options.exclusive,
- !!options.silent);
-
- let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, this, 'error', err);
- break;
- default:
- // Some other unexpected error was reported.
- if (ret < 0) {
- err = new NghttpError(ret);
- process.nextTick(emit, stream, 'error', err);
- }
- }
- debug(`[${sessionName(type)}] priority complete`);
+ this[kHandle].priority(options.parent | 0,
+ options.weight | 0,
+ !!options.exclusive,
+ !!options.silent);
}
// Submit an RST-STREAM frame to be sent to the remote peer.
// This will cause the Http2Stream to be closed.
-function submitRstStream(stream, code) {
- const type = this[kType];
- debug(`[${sessionName(type)}] submit actual rststream`);
+function submitRstStream(code) {
_unrefActive(this);
- const ret = this[kHandle].submitRstStream(stream[kID], code);
- let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, this, 'error', err);
- break;
- default:
- // Some other unexpected error was reported.
- if (ret < 0) {
- err = new NghttpError(ret);
- process.nextTick(emit, stream, 'error', err);
- break;
- }
- stream.destroy();
+ _unrefActive(this[kSession]);
+ const ret = this[kHandle].rstStream(code);
+ if (ret < 0) {
+ const err = new NghttpError(ret);
+ process.nextTick(emit, this, 'error', err);
+ return;
}
- debug(`[${sessionName(type)}] rststream complete`);
+ this.destroy();
}
-function doShutdown(self, options) {
- const handle = self[kHandle];
- const state = self[kState];
+function doShutdown(options) {
+ const handle = this[kHandle];
+ const state = this[kState];
if (handle === undefined || state.shutdown)
return; // Nothing to do, possibly because the session shutdown already.
- const ret = handle.submitGoaway(options.errorCode | 0,
- options.lastStreamID | 0,
- options.opaqueData);
+ const ret = handle.goaway(options.errorCode | 0,
+ options.lastStreamID | 0,
+ options.opaqueData);
state.shuttingDown = false;
state.shutdown = true;
if (ret < 0) {
- debug(`[${sessionName(self[kType])}] shutdown failed! code: ${ret}`);
+ debug(`[${sessionName(this[kType])}] shutdown failed! code: ${ret}`);
const err = new NghttpError(ret);
- process.nextTick(emit, self, 'error', err);
+ process.nextTick(emit, this, 'error', err);
return;
}
- process.nextTick(emit, self, 'shutdown', options);
- debug(`[${sessionName(self[kType])}] shutdown is complete`);
+ process.nextTick(emit, this, 'shutdown', options);
+ debug(`[${sessionName(this[kType])}] shutdown is complete`);
}
// Submit a graceful or immediate shutdown request for the Http2Session.
function submitShutdown(options) {
const type = this[kType];
debug(`[${sessionName(type)}] submitting actual shutdown request`);
- if (type === NGHTTP2_SESSION_SERVER &&
- options.graceful === true) {
+ if (type === NGHTTP2_SESSION_SERVER && options.graceful === true) {
// first send a shutdown notice
- this[kHandle].submitShutdownNotice();
+ this[kHandle].shutdownNotice();
// then, on flip of the event loop, do the actual shutdown
- setImmediate(doShutdown, this, options);
+ setImmediate(doShutdown.bind(this), options);
} else {
- doShutdown(this, options);
+ doShutdown.call(this, options);
}
}
-function finishSessionDestroy(self, socket) {
- const state = self[kState];
-
+function finishSessionDestroy(socket) {
if (!socket.destroyed)
socket.destroy();
+ const state = this[kState];
state.destroying = false;
state.destroyed = true;
// Destroy the handle
- const handle = self[kHandle];
- if (handle !== undefined) {
- handle.destroy(state.skipUnconsume);
- debug(`[${sessionName(self[kType])}] nghttp2session handle destroyed`);
+ if (this[kHandle] !== undefined) {
+ this[kHandle].destroy(state.skipUnconsume);
+ this[kHandle] = undefined;
}
- self[kHandle] = undefined;
- process.nextTick(emit, self, 'close');
- debug(`[${sessionName(self[kType])}] nghttp2session destroyed`);
+ process.nextTick(emit, this, 'close');
}
const proxySocketHandler = {
@@ -720,13 +643,16 @@ const proxySocketHandler = {
}
};
+function pingCallback(cb) {
+ return function(ack, duration, payload) {
+ const err = ack ? null : new errors.Error('ERR_HTTP2_PING_CANCEL');
+ cb(err, duration, payload);
+ };
+}
+
// Upon creation, the Http2Session takes ownership of the socket. The session
// may not be ready to use immediately if the socket is not yet fully connected.
class Http2Session extends EventEmitter {
-
- // type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT
- // options { Object }
- // socket { net.Socket | tls.TLSSocket | stream.Duplex }
constructor(type, options, socket) {
super();
@@ -785,10 +711,39 @@ class Http2Session extends EventEmitter {
// to something more reasonable because we may have any number
// of concurrent streams (2^31-1 is the upper limit on the number
// of streams)
- this.setMaxListeners((2 ** 31) - 1);
+ this.setMaxListeners(kMaxStreams);
debug(`[${sessionName(type)}] http2session created`);
}
+ setNextStreamID(id) {
+ if (typeof id !== 'number')
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'id', 'number');
+ if (id <= 0 || id > kMaxStreams)
+ throw new errors.RangeError('ERR_OUT_OF_RANGE');
+ this[kHandle].setNextStreamID(id);
+ }
+
+ ping(payload, callback) {
+ const state = this[kState];
+ if (state.destroyed || state.destroying)
+ throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+ if (typeof payload === 'function') {
+ callback = payload;
+ payload = undefined;
+ }
+ if (payload && !isArrayBufferView(payload)) {
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+ 'payload',
+ ['Buffer', 'TypedArray', 'DataView']);
+ }
+ if (payload && payload.length !== 8) {
+ throw new errors.RangeError('ERR_HTTP2_PING_LENGTH');
+ }
+ if (typeof callback !== 'function')
+ throw new errors.TypeError('ERR_INVALID_CALLBACK');
+ return this[kHandle].ping(payload, pingCallback(callback));
+ }
+
[kInspect](depth, opts) {
const state = this[kState];
const obj = {
@@ -830,9 +785,7 @@ class Http2Session extends EventEmitter {
// Retrieves state information for the Http2Session
get state() {
const handle = this[kHandle];
- return handle !== undefined ?
- getSessionState(handle) :
- Object.create(null);
+ return handle === undefined ? {} : getSessionState(handle);
}
// The settings currently in effect for the local peer. These will
@@ -844,7 +797,7 @@ class Http2Session extends EventEmitter {
const handle = this[kHandle];
if (handle === undefined)
- return Object.create(null);
+ return {};
settings = getSettings(handle, false); // Local
this[kLocalSettings] = settings;
@@ -859,7 +812,7 @@ class Http2Session extends EventEmitter {
const handle = this[kHandle];
if (handle === undefined)
- return Object.create(null);
+ return {};
settings = getSettings(handle, true); // Remote
this[kRemoteSettings] = settings;
@@ -886,7 +839,7 @@ class Http2Session extends EventEmitter {
16384, 2 ** 24 - 1);
assertWithinRange('maxConcurrentStreams',
settings.maxConcurrentStreams,
- 0, 2 ** 31 - 1);
+ 0, kMaxStreams);
assertWithinRange('maxHeaderListSize',
settings.maxHeaderListSize,
0, 2 ** 32 - 1);
@@ -913,83 +866,6 @@ class Http2Session extends EventEmitter {
submitSettings.call(this, settings);
}
- // Submits a PRIORITY frame to be sent to the remote peer.
- priority(stream, options) {
- const state = this[kState];
- if (state.destroyed || state.destroying)
- throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
-
- if (!(stream instanceof Http2Stream)) {
- throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
- 'stream',
- 'Http2Stream');
- }
- assertIsObject(options, 'options');
- options = Object.assign(Object.create(null), options);
- validatePriorityOptions(options);
-
- const id = stream[kID];
- debug(`[${sessionName(this[kType])}] sending priority for stream ` +
- `${id}`);
-
- // A stream cannot be made to depend on itself
- if (options.parent === id) {
- throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
- 'parent',
- options.parent);
- }
-
- if (id === undefined) {
- debug(`[${sessionName(this[kType])}] session still connecting. queue ` +
- 'priority');
- stream.once('ready', submitPriority.bind(this, stream, options));
- return;
- }
- submitPriority.call(this, stream, options);
- }
-
- // Submits an RST-STREAM frame to be sent to the remote peer. This will
- // cause the stream to be closed.
- rstStream(stream, code = NGHTTP2_NO_ERROR) {
- // Do not check destroying here, as the rstStream may be sent while
- // the session is in the middle of being destroyed.
- if (this[kState].destroyed)
- throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
-
- if (!(stream instanceof Http2Stream)) {
- throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
- 'stream',
- 'Http2Stream');
- }
-
- if (typeof code !== 'number') {
- throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
- 'code',
- 'number');
- }
-
- const state = stream[kState];
- if (state.rst) {
- // rst has already been called by self or peer,
- // do not call again
- return;
- }
- state.rst = true;
- state.rstCode = code;
-
- const id = stream[kID];
- debug(`[${sessionName(this[kType])}] initiating rststream for stream ` +
- `${id}: ${code}`);
-
- if (id === undefined) {
- debug(`[${sessionName(this[kType])}] session still connecting, queue ` +
- 'rststream');
- stream.once('ready', submitRstStream.bind(this, stream, code));
- return;
- }
- submitRstStream.call(this, stream, code);
- }
-
// Destroy the Http2Session
destroy() {
const state = this[kState];
@@ -1016,7 +892,7 @@ class Http2Session extends EventEmitter {
if (this[kHandle] !== undefined)
this[kHandle].destroying();
- setImmediate(finishSessionDestroy, this, socket);
+ setImmediate(finishSessionDestroy.bind(this), socket);
}
// Graceful or immediate shutdown of the Http2Session. Graceful shutdown
@@ -1040,7 +916,7 @@ class Http2Session extends EventEmitter {
options = Object.assign(Object.create(null), options);
if (options.opaqueData !== undefined &&
- !isUint8Array(options.opaqueData)) {
+ !isArrayBufferView(options.opaqueData)) {
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
'opaqueData',
options.opaqueData);
@@ -1177,7 +1053,7 @@ class ClientHttp2Session extends Http2Session {
options.getTrailers);
}
- const stream = new ClientHttp2Stream(this, undefined, {});
+ const stream = new ClientHttp2Stream(this, undefined, undefined, {});
const onConnect = requestOnConnect.bind(stream, headers, options);
@@ -1228,55 +1104,48 @@ function trackWriteState(stream, bytes) {
}
function afterDoStreamWrite(status, handle, req) {
- const session = handle[kOwner];
- _unrefActive(session);
+ const stream = handle[kOwner];
+ const session = stream[kSession];
+
+ _unrefActive(stream);
- const state = session[kState];
const { bytes } = req;
- state.writeQueueSize -= bytes;
+ stream[kState].writeQueueSize -= bytes;
- const stream = state.streams.get(req.stream);
- if (stream !== undefined) {
- _unrefActive(stream);
- stream[kState].writeQueueSize -= bytes;
+ if (session !== undefined) {
+ _unrefActive(session);
+ session[kState].writeQueueSize -= bytes;
}
if (typeof req.callback === 'function')
req.callback();
- this.handle = undefined;
+ req.handle = undefined;
}
function onHandleFinish() {
- const session = this[kSession];
- if (session === undefined) return;
if (this[kID] === undefined) {
this.once('ready', onHandleFinish);
} else {
- const handle = session[kHandle];
+ const handle = this[kHandle];
if (handle !== undefined) {
- // Shutdown on the next tick of the event loop just in case there is
- // still data pending in the outbound queue.
- assert(handle.shutdownStream(this[kID]) === undefined,
- `The stream ${this[kID]} does not exist. Please report this as ` +
- 'a bug in Node.js');
+ const req = new ShutdownWrap();
+ req.oncomplete = () => {};
+ req.handle = handle;
+ handle.shutdown(req);
}
}
}
function onSessionClose(hadError, code) {
abort(this);
- // Close the readable side
- this.push(null);
- // Close the writable side
- this.end();
+ this.push(null); // Close the readable side
+ this.end(); // Close the writable side
}
function onStreamClosed(code) {
abort(this);
- // Close the readable side
- this.push(null);
- // Close the writable side
- this.end();
+ this.push(null); // Close the readable side
+ this.end(); // Close the writable side
}
function streamOnResume() {
@@ -1284,27 +1153,15 @@ function streamOnResume() {
this.once('ready', streamOnResume);
return;
}
- const session = this[kSession];
- if (session) {
- assert(session[kHandle].streamReadStart(this[kID]) === undefined,
- `HTTP/2 Stream ${this[kID]} does not exist. Please report this as ` +
- 'a bug in Node.js');
- }
+ this[kHandle].readStart();
}
function streamOnPause() {
- const session = this[kSession];
- if (session) {
- assert(session[kHandle].streamReadStop(this[kID]) === undefined,
- `HTTP/2 Stream ${this[kID]} does not exist. Please report this as ' +
- 'a bug in Node.js`);
- }
+ this[kHandle].readStop();
}
-function handleFlushData(handle, streamID) {
- assert(handle.flushData(streamID) === undefined,
- `HTTP/2 Stream ${streamID} does not exist. Please report this as ` +
- 'a bug in Node.js');
+function handleFlushData(handle) {
+ handle.flushData();
}
function streamOnSessionConnect() {
@@ -1329,16 +1186,14 @@ function abort(stream) {
}
}
-// An Http2Stream is a Duplex stream. On the server-side, the Readable side
-// provides access to the received request data. On the client-side, the
-// Readable side provides access to the received response data. On the
-// server side, the writable side is used to transmit response data, while
-// on the client side it is used to transmit request data.
+// An Http2Stream is a Duplex stream that is backed by a
+// node::http2::Http2Stream handle implementing StreamBase.
class Http2Stream extends Duplex {
constructor(session, options) {
options.allowHalfOpen = true;
options.decodeStrings = false;
super(options);
+ this[async_id_symbol] = -1;
this.cork();
this[kSession] = session;
@@ -1368,8 +1223,14 @@ class Http2Stream extends Duplex {
debug(`[${sessionName(session[kType])}] http2stream created`);
}
- [kInit](id) {
+ [kInit](id, handle) {
this[kID] = id;
+ this[async_id_symbol] = handle.getAsyncId();
+ handle[kOwner] = this;
+ this[kHandle] = handle;
+ handle.ontrailers = onStreamTrailers;
+ handle.onstreamclose = onStreamClose;
+ handle.onread = onStreamRead;
this.emit('ready');
}
@@ -1434,8 +1295,8 @@ class Http2Stream extends Duplex {
get state() {
const id = this[kID];
if (this.destroyed || id === undefined)
- return Object.create(null);
- return getStreamState(this[kSession][kHandle], id);
+ return {};
+ return getStreamState(this[kHandle], id);
}
[kProceed]() {
@@ -1449,12 +1310,14 @@ class Http2Stream extends Duplex {
this.once('ready', this._write.bind(this, data, encoding, cb));
return;
}
+
+ _unrefActive(this);
+ _unrefActive(this[kSession]);
+
if (!this[kState].headersSent)
this[kProceed]();
- const session = this[kSession];
- _unrefActive(this);
- _unrefActive(session);
- const handle = session[kHandle];
+
+ const handle = this[kHandle];
const req = new WriteWrap();
req.stream = this[kID];
req.handle = handle;
@@ -1472,12 +1335,14 @@ class Http2Stream extends Duplex {
this.once('ready', this._writev.bind(this, data, cb));
return;
}
+
+ _unrefActive(this);
+ _unrefActive(this[kSession]);
+
if (!this[kState].headersSent)
this[kProceed]();
- const session = this[kSession];
- _unrefActive(this);
- _unrefActive(session);
- const handle = session[kHandle];
+
+ const handle = this[kHandle];
const req = new WriteWrap();
req.stream = this[kID];
req.handle = handle;
@@ -1506,7 +1371,7 @@ class Http2Stream extends Duplex {
return;
}
_unrefActive(this);
- process.nextTick(handleFlushData, this[kSession][kHandle], this[kID]);
+ process.nextTick(handleFlushData, this[kHandle]);
}
// Submits an RST-STREAM frame to shutdown this stream.
@@ -1515,19 +1380,32 @@ class Http2Stream extends Duplex {
// After sending the rstStream, this.destroy() will be called making
// the stream object no longer usable.
rstStream(code = NGHTTP2_NO_ERROR) {
- if (this.destroyed)
- throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
- const session = this[kSession];
+ if (typeof code !== 'number')
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'code', 'number');
+
if (this[kID] === undefined) {
- debug(
- `[${sessionName(session[kType])}] queuing rstStream for new stream`);
this.once('ready', this.rstStream.bind(this, code));
return;
}
- debug(`[${sessionName(session[kType])}] sending rstStream for stream ` +
- `${this[kID]}: ${code}`);
+
+ const state = this[kState];
+ if (state.rst) {
+ // rst has already been set by self or peer, do not set again
+ return;
+ }
+ state.rst = true;
+ state.rstCode = code;
+
_unrefActive(this);
- session.rstStream(this, code);
+ _unrefActive(this[kSession]);
+
+ const id = this[kID];
+
+ if (id === undefined) {
+ this.once('ready', submitRstStream.bind(this, code));
+ return;
+ }
+ submitRstStream.call(this, code);
}
rstWithNoError() {
@@ -1550,12 +1428,6 @@ class Http2Stream extends Duplex {
this.rstStream(NGHTTP2_INTERNAL_ERROR);
}
- // Note that this (and other methods like additionalHeaders and rstStream)
- // cause nghttp to queue frames up in its internal buffer that are not
- // actually sent on the wire until the next tick of the event loop. The
- // semantics of this method then are: queue a priority frame to be sent and
- // not immediately send the priority frame. There is current no callback
- // triggered when the data is actually sent.
priority(options) {
if (this.destroyed)
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
@@ -1568,7 +1440,27 @@ class Http2Stream extends Duplex {
debug(`[${sessionName(session[kType])}] sending priority for stream ` +
`${this[kID]}`);
_unrefActive(this);
- session.priority(this, options);
+
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+ validatePriorityOptions(options);
+
+ const id = this[kID];
+ debug(`[${sessionName(session[kType])}] sending priority for stream ` +
+ `${id}`);
+
+ // A stream cannot be made to depend on itself
+ if (options.parent === id) {
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'parent',
+ options.parent);
+ }
+
+ if (id === undefined) {
+ this.once('ready', submitPriority.bind(this, options));
+ return;
+ }
+ submitPriority.call(this, options);
}
// Called by this.destroy().
@@ -1594,15 +1486,15 @@ class Http2Stream extends Duplex {
server.emit('streamError', err, this);
}
- process.nextTick(continueStreamDestroy, this, err, callback);
+ process.nextTick(continueStreamDestroy.bind(this), err, callback);
}
}
-function continueStreamDestroy(self, err, callback) {
- const session = self[kSession];
- const state = self[kState];
+function continueStreamDestroy(err, callback) {
+ const session = this[kSession];
+ const state = this[kState];
- debug(`[${sessionName(session[kType])}] destroying stream ${self[kID]}`);
+ debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`);
// Submit RST-STREAM frame if one hasn't been sent already and the
// stream hasn't closed normally...
@@ -1610,16 +1502,16 @@ function continueStreamDestroy(self, err, callback) {
let code = state.rstCode;
if (!rst && !session.destroyed) {
code = err instanceof Error ? NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
- session.rstStream(self, code);
+ this.rstStream(code);
}
// Remove the close handler on the session
session.removeListener('close', state.closeHandler);
// Unenroll the timer
- self.setTimeout(0);
+ this.setTimeout(0);
- setImmediate(finishStreamDestroy, self, session[kHandle]);
+ setImmediate(finishStreamDestroy.bind(this));
// RST code 8 not emitted as an error as its used by clients to signify
// abort and is already covered by aborted event, also allows more
@@ -1628,17 +1520,20 @@ function continueStreamDestroy(self, err, callback) {
err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
}
callback(err);
- process.nextTick(emit, self, 'streamClosed', code);
- debug(`[${sessionName(session[kType])}] stream ${self[kID]} destroyed`);
+ process.nextTick(emit, this, 'streamClosed', code);
+ debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
}
-function finishStreamDestroy(self, handle) {
- const id = self[kID];
- self[kSession][kState].streams.delete(id);
- delete self[kSession];
- if (handle !== undefined)
- handle.destroyStream(id);
- self.emit('destroy');
+function finishStreamDestroy() {
+ const id = this[kID];
+ this[kSession][kState].streams.delete(id);
+ this[kSession] = undefined;
+ const handle = this[kHandle];
+ if (handle !== undefined) {
+ this[kHandle] = undefined;
+ handle.destroy();
+ }
+ this.emit('destroy');
}
function processHeaders(headers) {
@@ -1664,32 +1559,25 @@ function processHeaders(headers) {
function processRespondWithFD(fd, headers, offset = 0, length = -1,
streamOptions = 0) {
- const session = this[kSession];
const state = this[kState];
state.headersSent = true;
// Close the writable side of the stream
this.end();
- const ret = session[kHandle].submitFile(this[kID], fd, headers,
- offset, length, streamOptions);
- let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, session, 'error', err);
- break;
- default:
- if (ret < 0) {
- err = new NghttpError(ret);
- process.nextTick(emit, this, 'error', err);
- break;
- }
- // exact length of the file doesn't matter here, since the
- // stream is closing anyway — just use 1 to signify that
- // a write does exist
- trackWriteState(this, 1);
+ const ret = this[kHandle].respondFD(fd, headers,
+ offset, length,
+ streamOptions);
+
+ if (ret < 0) {
+ const err = new NghttpError(ret);
+ process.nextTick(emit, this, 'error', err);
+ return;
}
+ // exact length of the file doesn't matter here, since the
+ // stream is closing anyway — just use 1 to signify that
+ // a write does exist
+ trackWriteState(this, 1);
}
function doSendFD(session, options, fd, headers, streamOptions, err, stat) {
@@ -1813,9 +1701,9 @@ function streamOnError(err) {
class ServerHttp2Stream extends Http2Stream {
- constructor(session, id, options, headers) {
+ constructor(session, handle, id, options, headers) {
super(session, options);
- this[kInit](id);
+ this[kInit](id, handle);
this[kProtocol] = headers[HTTP2_HEADER_SCHEME];
this[kAuthority] = headers[HTTP2_HEADER_AUTHORITY];
this.on('error', streamOnError);
@@ -1839,6 +1727,9 @@ class ServerHttp2Stream extends Http2Stream {
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
const session = this[kSession];
+ if (!session.remoteSettings.enablePush)
+ throw new errors.Error('ERR_HTTP2_PUSH_DISABLED');
+
debug(`[${sessionName(session[kType])}] initiating push stream for stream` +
` ${this[kID]}`);
@@ -1846,9 +1737,6 @@ class ServerHttp2Stream extends Http2Stream {
const state = session[kState];
const streams = state.streams;
- if (!session.remoteSettings.enablePush)
- throw new errors.Error('ERR_HTTP2_PUSH_DISABLED');
-
if (typeof options === 'function') {
callback = options;
options = undefined;
@@ -1887,45 +1775,40 @@ class ServerHttp2Stream extends Http2Stream {
const streamOptions = options.endStream ? STREAM_OPTION_EMPTY_PAYLOAD : 0;
- const ret = session[kHandle].submitPushPromise(this[kID],
- headersList,
- streamOptions);
+ const ret = this[kHandle].pushPromise(headersList, streamOptions);
let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, session, 'error', err);
- break;
- case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
- err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
- process.nextTick(emit, session, 'error', err);
- break;
- case NGHTTP2_ERR_STREAM_CLOSED:
- err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
- process.nextTick(emit, this, 'error', err);
- break;
- default:
- if (ret <= 0) {
+ if (typeof ret === 'number') {
+ switch (ret) {
+ case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
+ err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
+ break;
+ case NGHTTP2_ERR_STREAM_CLOSED:
+ err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+ break;
+ default:
err = new NghttpError(ret);
- process.nextTick(emit, this, 'error', err);
break;
- }
- debug(`[${sessionName(session[kType])}] push stream ${ret} created`);
- options.readable = !options.endStream;
+ }
+ process.nextTick(emit, this, 'error', err);
+ return;
+ }
- const stream = new ServerHttp2Stream(session, ret, options, headers);
+ const id = ret.id();
+ debug(`[${sessionName(session[kType])}] push stream ${id} created`);
+ options.readable = !options.endStream;
- // If the push stream is a head request, close the writable side of
- // the stream immediately as there won't be any data sent.
- if (headRequest) {
- stream.end();
- const state = stream[kState];
- state.headRequest = true;
- }
+ const stream = new ServerHttp2Stream(session, ret, id, options, headers);
+ streams.set(id, stream);
- streams.set(ret, stream);
- process.nextTick(callback, stream, headers, 0);
+ // If the push stream is a head request, close the writable side of
+ // the stream immediately as there won't be any data sent.
+ if (headRequest) {
+ stream.end();
+ const state = stream[kState];
+ state.headRequest = true;
}
+
+ process.nextTick(callback, stream, headers, 0);
}
// Initiate a response on this Http2Stream
@@ -1983,20 +1866,10 @@ class ServerHttp2Stream extends Http2Stream {
if (options.endStream)
this.end();
- const ret = session[kHandle].submitResponse(this[kID],
- headersList,
- streamOptions);
- let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, session, 'error', err);
- break;
- default:
- if (ret < 0) {
- err = new NghttpError(ret);
- process.nextTick(emit, this, 'error', err);
- }
+ const ret = this[kHandle].respond(headersList, streamOptions);
+ if (ret < 0) {
+ const err = new NghttpError(ret);
+ process.nextTick(emit, this, 'error', err);
}
}
@@ -2182,18 +2055,10 @@ class ServerHttp2Stream extends Http2Stream {
throw headersList;
}
- const ret = session[kHandle].sendHeaders(this[kID], headersList);
- let err;
- switch (ret) {
- case NGHTTP2_ERR_NOMEM:
- err = new errors.Error('ERR_OUTOFMEMORY');
- process.nextTick(emit, session, 'error', err);
- break;
- default:
- if (ret < 0) {
- err = new NghttpError(ret);
- process.nextTick(emit, this, 'error', err);
- }
+ const ret = this[kHandle].info(headersList);
+ if (ret < 0) {
+ const err = new NghttpError(ret);
+ process.nextTick(emit, this, 'error', err);
}
}
}
@@ -2201,11 +2066,11 @@ class ServerHttp2Stream extends Http2Stream {
ServerHttp2Stream.prototype[kProceed] = ServerHttp2Stream.prototype.respond;
class ClientHttp2Stream extends Http2Stream {
- constructor(session, id, options) {
+ constructor(session, handle, id, options) {
super(session, options);
this[kState].headersSent = true;
if (id !== undefined)
- this[kInit](id);
+ this[kInit](id, handle);
this.on('headers', handleHeaderContinue);
debug(`[${sessionName(session[kType])}] clienthttp2stream created`);
}
@@ -2452,7 +2317,7 @@ function setupCompat(ev) {
}
}
-function socketOnClose(hadError) {
+function socketOnClose() {
const session = this[kSession];
if (session !== undefined && !session.destroyed) {
// Skip unconsume because the socket is destroyed.
@@ -2464,7 +2329,7 @@ function socketOnClose(hadError) {
// If the session emits an error, forward it to the socket as a sessionError;
// failing that, destroy the session, remove the listener and re-emit the error
function clientSessionOnError(error) {
- debug(`client session error: ${error.message}`);
+ debug(`[${sessionName(this[kType])}] session error: ${error.message}`);
if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error))
return;
this.destroy();
@@ -2571,7 +2436,7 @@ function getPackedSettings(settings) {
16384, 2 ** 24 - 1);
assertWithinRange('maxConcurrentStreams',
settings.maxConcurrentStreams,
- 0, 2 ** 31 - 1);
+ 0, kMaxStreams);
assertWithinRange('maxHeaderListSize',
settings.maxHeaderListSize,
0, 2 ** 32 - 1);
@@ -2587,9 +2452,9 @@ function getPackedSettings(settings) {
}
function getUnpackedSettings(buf, options = {}) {
- if (!isUint8Array(buf)) {
+ if (!isArrayBufferView(buf)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buf',
- ['Buffer', 'Uint8Array']);
+ ['Buffer', 'TypedArray', 'DataView']);
}
if (buf.length % 6 !== 0)
throw new errors.RangeError('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH');
@@ -2637,7 +2502,7 @@ function getUnpackedSettings(buf, options = {}) {
16384, 2 ** 24 - 1);
assertWithinRange('maxConcurrentStreams',
settings.maxConcurrentStreams,
- 0, 2 ** 31 - 1);
+ 0, kMaxStreams);
assertWithinRange('maxHeaderListSize',
settings.maxHeaderListSize,
0, 2 ** 32 - 1);
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index e6da4293b5..1f9853aa1f 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -173,7 +173,8 @@ const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2;
const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3;
const IDX_OPTIONS_PADDING_STRATEGY = 4;
const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
-const IDX_OPTIONS_FLAGS = 6;
+const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
+const IDX_OPTIONS_FLAGS = 7;
function updateOptionsBuffer(options) {
var flags = 0;
@@ -207,6 +208,11 @@ function updateOptionsBuffer(options) {
optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS] =
options.maxHeaderListPairs;
}
+ if (typeof options.maxOutstandingPings === 'number') {
+ flags |= (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS);
+ optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] =
+ options.maxOutstandingPings;
+ }
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
}
@@ -259,25 +265,19 @@ function getDefaultSettings() {
// remote is a boolean. true to fetch remote settings, false to fetch local.
// this is only called internally
function getSettings(session, remote) {
- const holder = Object.create(null);
if (remote)
- session.refreshRemoteSettings();
+ session.remoteSettings();
else
- session.refreshLocalSettings();
-
- holder.headerTableSize =
- settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE];
- holder.enablePush =
- !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH];
- holder.initialWindowSize =
- settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE];
- holder.maxFrameSize =
- settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE];
- holder.maxConcurrentStreams =
- settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS];
- holder.maxHeaderListSize =
- settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
- return holder;
+ session.localSettings();
+
+ return {
+ headerTableSize: settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE],
+ enablePush: !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH],
+ initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE],
+ maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE],
+ maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS],
+ maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]
+ };
}
function updateSettingsBuffer(settings) {
@@ -316,45 +316,39 @@ function updateSettingsBuffer(settings) {
}
function getSessionState(session) {
- const holder = Object.create(null);
- binding.refreshSessionState(session);
- holder.effectiveLocalWindowSize =
- sessionState[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE];
- holder.effectiveRecvDataLength =
- sessionState[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH];
- holder.nextStreamID =
- sessionState[IDX_SESSION_STATE_NEXT_STREAM_ID];
- holder.localWindowSize =
- sessionState[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE];
- holder.lastProcStreamID =
- sessionState[IDX_SESSION_STATE_LAST_PROC_STREAM_ID];
- holder.remoteWindowSize =
- sessionState[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE];
- holder.outboundQueueSize =
- sessionState[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE];
- holder.deflateDynamicTableSize =
- sessionState[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE];
- holder.inflateDynamicTableSize =
- sessionState[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE];
- return holder;
+ session.refreshState();
+ return {
+ effectiveLocalWindowSize:
+ sessionState[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE],
+ effectiveRecvDataLength:
+ sessionState[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH],
+ nextStreamID:
+ sessionState[IDX_SESSION_STATE_NEXT_STREAM_ID],
+ localWindowSize:
+ sessionState[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE],
+ lastProcStreamID:
+ sessionState[IDX_SESSION_STATE_LAST_PROC_STREAM_ID],
+ remoteWindowSize:
+ sessionState[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE],
+ outboundQueueSize:
+ sessionState[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE],
+ deflateDynamicTableSize:
+ sessionState[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE],
+ inflateDynamicTableSize:
+ sessionState[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE]
+ };
}
-function getStreamState(session, stream) {
- const holder = Object.create(null);
- binding.refreshStreamState(session, stream);
- holder.state =
- streamState[IDX_STREAM_STATE];
- holder.weight =
- streamState[IDX_STREAM_STATE_WEIGHT];
- holder.sumDependencyWeight =
- streamState[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT];
- holder.localClose =
- streamState[IDX_STREAM_STATE_LOCAL_CLOSE];
- holder.remoteClose =
- streamState[IDX_STREAM_STATE_REMOTE_CLOSE];
- holder.localWindowSize =
- streamState[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE];
- return holder;
+function getStreamState(stream) {
+ stream.refreshState();
+ return {
+ state: streamState[IDX_STREAM_STATE],
+ weight: streamState[IDX_STREAM_STATE_WEIGHT],
+ sumDependencyWeight: streamState[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT],
+ localClose: streamState[IDX_STREAM_STATE_LOCAL_CLOSE],
+ remoteClose: streamState[IDX_STREAM_STATE_REMOTE_CLOSE],
+ localWindowSize: streamState[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE]
+ };
}
function isIllegalConnectionSpecificHeader(name, value) {
diff --git a/node.gyp b/node.gyp
index 38241302e2..c32adc3720 100644
--- a/node.gyp
+++ b/node.gyp
@@ -257,8 +257,6 @@
'src/js_stream.h',
'src/module_wrap.h',
'src/node.h',
- 'src/node_http2_core.h',
- 'src/node_http2_core-inl.h',
'src/node_buffer.h',
'src/node_constants.h',
'src/node_debug_options.h',
diff --git a/src/async_wrap.h b/src/async_wrap.h
index f58f944327..98451ead3b 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -41,7 +41,8 @@ namespace node {
V(GETADDRINFOREQWRAP) \
V(GETNAMEINFOREQWRAP) \
V(HTTP2SESSION) \
- V(HTTP2SESSIONSHUTDOWNWRAP) \
+ V(HTTP2STREAM) \
+ V(HTTP2PING) \
V(HTTPPARSER) \
V(JSSTREAM) \
V(PIPECONNECTWRAP) \
diff --git a/src/env.h b/src/env.h
index 4eadc7aafe..42903b0c46 100644
--- a/src/env.h
+++ b/src/env.h
@@ -310,6 +310,8 @@ class ModuleWrap;
V(buffer_prototype_object, v8::Object) \
V(context, v8::Context) \
V(domains_stack_array, v8::Array) \
+ V(http2ping_constructor_template, v8::ObjectTemplate) \
+ V(http2stream_constructor_template, v8::ObjectTemplate) \
V(inspector_console_api_object, v8::Object) \
V(module_load_list_array, v8::Array) \
V(pbkdf2_constructor_template, v8::ObjectTemplate) \
diff --git a/src/node_http2.cc b/src/node_http2.cc
index bdf0d31b47..b439ae588a 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -14,6 +14,8 @@ using v8::Context;
using v8::Float64Array;
using v8::Function;
using v8::Integer;
+using v8::Number;
+using v8::ObjectTemplate;
using v8::String;
using v8::Uint32;
using v8::Uint32Array;
@@ -21,10 +23,11 @@ using v8::Undefined;
namespace http2 {
-Nghttp2Session::Callbacks Nghttp2Session::callback_struct_saved[2] = {
+const Http2Session::Callbacks Http2Session::callback_struct_saved[2] = {
Callbacks(false),
Callbacks(true)};
+
Http2Options::Http2Options(Environment* env) {
nghttp2_option_new(&options_);
@@ -70,8 +73,13 @@ Http2Options::Http2Options(Environment* env) {
if (flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS)) {
SetMaxHeaderPairs(buffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS]);
}
+
+ if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)) {
+ SetMaxOutstandingPings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS]);
+ }
}
+
Http2Settings::Http2Settings(Environment* env) : env_(env) {
entries_.AllocateSufficientStorage(IDX_SETTINGS_COUNT);
AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
@@ -82,7 +90,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
if (flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) {
uint32_t val = buffer[IDX_SETTINGS_HEADER_TABLE_SIZE];
- DEBUG_HTTP2("Setting header table size: %d\n", val);
+ DEBUG_HTTP2("Http2Settings: setting header table size: %d\n", val);
entries_[n].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE;
entries_[n].value = val;
n++;
@@ -90,7 +98,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
if (flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) {
uint32_t val = buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS];
- DEBUG_HTTP2("Setting max concurrent streams: %d\n", val);
+ DEBUG_HTTP2("Http2Settings: setting max concurrent streams: %d\n", val);
entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS;
entries_[n].value = val;
n++;
@@ -98,7 +106,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
if (flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) {
uint32_t val = buffer[IDX_SETTINGS_MAX_FRAME_SIZE];
- DEBUG_HTTP2("Setting max frame size: %d\n", val);
+ DEBUG_HTTP2("Http2Settings: setting max frame size: %d\n", val);
entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_FRAME_SIZE;
entries_[n].value = val;
n++;
@@ -106,7 +114,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
if (flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) {
uint32_t val = buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE];
- DEBUG_HTTP2("Setting initial window size: %d\n", val);
+ DEBUG_HTTP2("Http2Settings: setting initial window size: %d\n", val);
entries_[n].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
entries_[n].value = val;
n++;
@@ -114,7 +122,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
if (flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) {
uint32_t val = buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
- DEBUG_HTTP2("Setting max header list size: %d\n", val);
+ DEBUG_HTTP2("Http2Settings: setting max header list size: %d\n", val);
entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE;
entries_[n].value = val;
n++;
@@ -122,7 +130,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
if (flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) {
uint32_t val = buffer[IDX_SETTINGS_ENABLE_PUSH];
- DEBUG_HTTP2("Setting enable push: %d\n", val);
+ DEBUG_HTTP2("Http2Settings: setting enable push: %d\n", val);
entries_[n].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH;
entries_[n].value = val;
n++;
@@ -131,6 +139,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
count_ = n;
}
+
inline Local<Value> Http2Settings::Pack() {
const size_t len = count_ * 6;
Local<Value> buf = Buffer::New(env_, len).ToLocalChecked();
@@ -144,6 +153,7 @@ inline Local<Value> Http2Settings::Pack() {
return Undefined(env_->isolate());
}
+
inline void Http2Settings::Update(Environment* env,
Http2Session* session,
get_setting fn) {
@@ -186,6 +196,7 @@ inline void Http2Settings::RefreshDefaults(Environment* env) {
(1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE);
}
+
Http2Priority::Http2Priority(Environment* env,
Local<Value> parent,
Local<Value> weight,
@@ -199,27 +210,162 @@ Http2Priority::Http2Priority(Environment* env,
nghttp2_priority_spec_init(&spec, parent_, weight_, exclusive_ ? 1 : 0);
}
+
+inline const char* Http2Session::TypeName() {
+ switch (session_type_) {
+ case NGHTTP2_SESSION_SERVER: return "server";
+ case NGHTTP2_SESSION_CLIENT: return "client";
+ default:
+ // This should never happen
+ ABORT();
+ }
+}
+
+
+Headers::Headers(Isolate* isolate,
+ Local<Context> context,
+ Local<Array> headers) {
+ Local<Value> header_string = headers->Get(context, 0).ToLocalChecked();
+ Local<Value> header_count = headers->Get(context, 1).ToLocalChecked();
+ count_ = header_count.As<Uint32>()->Value();
+ int header_string_len = header_string.As<String>()->Length();
+
+ if (count_ == 0) {
+ CHECK_EQ(header_string_len, 0);
+ return;
+ }
+
+ // Allocate a single buffer with count_ nghttp2_nv structs, followed
+ // by the raw header data as passed from JS. This looks like:
+ // | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents |
+ buf_.AllocateSufficientStorage((alignof(nghttp2_nv) - 1) +
+ count_ * sizeof(nghttp2_nv) +
+ header_string_len);
+ // Make sure the start address is aligned appropriately for an nghttp2_nv*.
+ char* start = reinterpret_cast<char*>(
+ ROUND_UP(reinterpret_cast<uintptr_t>(*buf_), alignof(nghttp2_nv)));
+ char* header_contents = start + (count_ * sizeof(nghttp2_nv));
+ nghttp2_nv* const nva = reinterpret_cast<nghttp2_nv*>(start);
+
+ CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length());
+ CHECK_EQ(header_string.As<String>()
+ ->WriteOneByte(reinterpret_cast<uint8_t*>(header_contents),
+ 0, header_string_len,
+ String::NO_NULL_TERMINATION),
+ header_string_len);
+
+ size_t n = 0;
+ char* p;
+ for (p = header_contents; p < header_contents + header_string_len; n++) {
+ if (n >= count_) {
+ // This can happen if a passed header contained a null byte. In that
+ // case, just provide nghttp2 with an invalid header to make it reject
+ // the headers list.
+ static uint8_t zero = '\0';
+ nva[0].name = nva[0].value = &zero;
+ nva[0].namelen = nva[0].valuelen = 1;
+ count_ = 1;
+ return;
+ }
+
+ nva[n].flags = NGHTTP2_NV_FLAG_NONE;
+ nva[n].name = reinterpret_cast<uint8_t*>(p);
+ nva[n].namelen = strlen(p);
+ p += nva[n].namelen + 1;
+ nva[n].value = reinterpret_cast<uint8_t*>(p);
+ nva[n].valuelen = strlen(p);
+ p += nva[n].valuelen + 1;
+ }
+}
+
+
+Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) {
+ CHECK_EQ(nghttp2_session_callbacks_new(&callbacks), 0);
+ nghttp2_session_callbacks_set_on_begin_headers_callback(
+ callbacks, OnBeginHeadersCallback);
+ nghttp2_session_callbacks_set_on_header_callback2(
+ callbacks, OnHeaderCallback);
+ nghttp2_session_callbacks_set_on_frame_recv_callback(
+ callbacks, OnFrameReceive);
+ nghttp2_session_callbacks_set_on_stream_close_callback(
+ callbacks, OnStreamClose);
+ nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
+ callbacks, OnDataChunkReceived);
+ nghttp2_session_callbacks_set_on_frame_not_send_callback(
+ callbacks, OnFrameNotSent);
+ nghttp2_session_callbacks_set_on_invalid_header_callback2(
+ callbacks, OnInvalidHeader);
+ nghttp2_session_callbacks_set_error_callback(
+ callbacks, OnNghttpError);
+
+ if (kHasGetPaddingCallback) {
+ nghttp2_session_callbacks_set_select_padding_callback(
+ callbacks, OnSelectPadding);
+ }
+}
+
+
+Http2Session::Callbacks::~Callbacks() {
+ nghttp2_session_callbacks_del(callbacks);
+}
+
+
Http2Session::Http2Session(Environment* env,
Local<Object> wrap,
nghttp2_session_type type)
: AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTP2SESSION),
- StreamBase(env) {
+ session_type_(type) {
MakeWeak<Http2Session>(this);
Http2Options opts(env);
+ int32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
+ max_header_pairs_ =
+ type == NGHTTP2_SESSION_SERVER
+ ? std::max(maxHeaderPairs, 4) // minimum # of request headers
+ : std::max(maxHeaderPairs, 1); // minimum # of response headers
+
+ max_outstanding_pings_ = opts.GetMaxOutstandingPings();
+
padding_strategy_ = opts.GetPaddingStrategy();
- int32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
- maxHeaderPairs = type == NGHTTP2_SESSION_SERVER ?
- std::max(maxHeaderPairs, 4) : std::max(maxHeaderPairs, 1);
- Init(type, *opts, nullptr, maxHeaderPairs);
+ bool hasGetPaddingCallback =
+ padding_strategy_ == PADDING_STRATEGY_MAX ||
+ padding_strategy_ == PADDING_STRATEGY_CALLBACK;
+
+ nghttp2_session_callbacks* callbacks
+ = callback_struct_saved[hasGetPaddingCallback ? 1 : 0].callbacks;
+
+ auto fn = type == NGHTTP2_SESSION_SERVER ?
+ nghttp2_session_server_new2 :
+ nghttp2_session_client_new2;
+
+ // This should fail only if the system is out of memory, which
+ // is going to cause lots of other problems anyway, or if any
+ // of the options are out of acceptable range, which we should
+ // be catching before it gets this far. Either way, crash if this
+ // fails.
+ CHECK_EQ(fn(&session_, callbacks, this, *opts), 0);
+
+ Start();
+}
+
+
+Http2Session::~Http2Session() {
+ CHECK(persistent().IsEmpty());
+ Close();
+}
- // For every node::Http2Session instance, there is a uv_prepare_t handle
- // whose callback is triggered on every tick of the event loop. When
- // run, nghttp2 is prompted to send any queued data it may have stored.
+// For every node::Http2Session instance, there is a uv_prepare_t handle
+// whose callback is triggered on every tick of the event loop. When
+// run, nghttp2 is prompted to send any queued data it may have stored.
+// TODO(jasnell): Currently, this creates one uv_prepare_t per Http2Session,
+// we should investigate to see if it's faster to create a
+// single uv_prepare_t for all Http2Sessions, then iterate
+// over each.
+void Http2Session::Start() {
prep_ = new uv_prepare_t();
- uv_prepare_init(env->event_loop(), prep_);
+ uv_prepare_init(env()->event_loop(), prep_);
prep_->data = static_cast<void*>(this);
uv_prepare_start(prep_, [](uv_prepare_t* t) {
Http2Session* session = static_cast<Http2Session*>(t->data);
@@ -233,39 +379,69 @@ Http2Session::Http2Session(Environment* env,
});
}
-Http2Session::~Http2Session() {
- CHECK(persistent().IsEmpty());
- Close();
+// Stop the uv_prep_t from further activity, destroy the handle
+void Http2Session::Stop() {
+ DEBUG_HTTP2SESSION(this, "stopping uv_prep_t handle");
+ CHECK_EQ(uv_prepare_stop(prep_), 0);
+ auto prep_close = [](uv_handle_t* handle) {
+ delete reinterpret_cast<uv_prepare_t*>(handle);
+ };
+ uv_close(reinterpret_cast<uv_handle_t*>(prep_), prep_close);
+ prep_ = nullptr;
}
+
void Http2Session::Close() {
+ DEBUG_HTTP2SESSION(this, "closing session");
if (!object().IsEmpty())
ClearWrap(object());
persistent().Reset();
- this->Nghttp2Session::Close();
- // Stop the loop
- CHECK_EQ(uv_prepare_stop(prep_), 0);
- auto prep_close = [](uv_handle_t* handle) {
- delete reinterpret_cast<uv_prepare_t*>(handle);
- };
- uv_close(reinterpret_cast<uv_handle_t*>(prep_), prep_close);
- prep_ = nullptr;
+ if (session_ == nullptr)
+ return;
+
+ CHECK_EQ(nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR), 0);
+ nghttp2_session_del(session_);
+ session_ = nullptr;
+
+ while (!outstanding_pings_.empty()) {
+ Http2Session::Http2Ping* ping = PopPing();
+ ping->Done(false);
+ }
+
+ Stop();
+}
+
+
+inline Http2Stream* Http2Session::FindStream(int32_t id) {
+ auto s = streams_.find(id);
+ return s != streams_.end() ? s->second : nullptr;
+}
+
+
+inline void Http2Session::AddStream(Http2Stream* stream) {
+ streams_[stream->id()] = stream;
+}
+
+
+inline void Http2Session::RemoveStream(int32_t id) {
+ streams_.erase(id);
}
-ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
- size_t maxPayloadLen) {
- DEBUG_HTTP2("Http2Session: using max frame size padding\n");
+
+inline ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
+ size_t maxPayloadLen) {
+ DEBUG_HTTP2SESSION2(this, "using max frame size padding: %d", maxPayloadLen);
return maxPayloadLen;
}
-ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
- size_t maxPayloadLen) {
- DEBUG_HTTP2("Http2Session: using callback padding\n");
- Isolate* isolate = env()->isolate();
- Local<Context> context = env()->context();
+inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
+ size_t maxPayloadLen) {
+ DEBUG_HTTP2SESSION(this, "using callback to determine padding");
+ Isolate* isolate = env()->isolate();
HandleScope handle_scope(isolate);
+ Local<Context> context = env()->context();
Context::Scope context_scope(context);
#if defined(DEBUG) && DEBUG
@@ -281,26 +457,1141 @@ ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
uint32_t retval = buffer[PADDING_BUF_RETURN_VALUE];
retval = std::min(retval, static_cast<uint32_t>(maxPayloadLen));
retval = std::max(retval, static_cast<uint32_t>(frameLen));
+ DEBUG_HTTP2SESSION2(this, "using padding size %d", retval);
return retval;
}
-void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- nghttp2_session* s = session->session();
- int32_t id = args[0]->Int32Value(env->context()).ToChecked();
- DEBUG_HTTP2("Http2Session: setting next stream id to %d\n", id);
- nghttp2_session_set_next_stream_id(s, id);
+
+// Submits a graceful shutdown notice to nghttp
+// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html
+inline void Http2Session::SubmitShutdownNotice() {
+ // Only an HTTP2 Server is permitted to send a shutdown notice
+ if (session_type_ == NGHTTP2_SESSION_CLIENT)
+ return;
+ DEBUG_HTTP2SESSION(this, "sending shutdown notice");
+ // The only situation where this should fail is if the system is
+ // out of memory, which will cause other problems. Go ahead and crash
+ // in that case.
+ CHECK_EQ(nghttp2_submit_shutdown_notice(session_), 0);
+}
+
+
+// Note: This *must* send a SETTINGS frame even if niv == 0
+inline void Http2Session::Settings(const nghttp2_settings_entry iv[],
+ size_t niv) {
+ DEBUG_HTTP2SESSION2(this, "submitting %d settings", niv);
+ // This will fail either if the system is out of memory, or if the settings
+ // values are not within the appropriate range. We should be catching the
+ // latter before it gets this far so crash in either case.
+ CHECK_EQ(nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv), 0);
+}
+
+
+// Write data received from the i/o stream to the underlying nghttp2_session.
+inline ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) {
+ size_t total = 0;
+ // Note that nghttp2_session_mem_recv is a synchronous operation that
+ // will trigger a number of other callbacks. Those will, in turn have
+ // multiple side effects.
+ for (size_t n = 0; n < nbufs; n++) {
+ ssize_t ret =
+ nghttp2_session_mem_recv(session_,
+ reinterpret_cast<uint8_t*>(bufs[n].base),
+ bufs[n].len);
+ CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
+
+ if (ret < 0)
+ return ret;
+
+ total += ret;
+ }
+ // Send any data that was queued up while processing the received data.
+ SendPendingData();
+ return total;
+}
+
+
+inline int32_t GetFrameID(const nghttp2_frame* frame) {
+ // If this is a push promise, we want to grab the id of the promised stream
+ return (frame->hd.type == NGHTTP2_PUSH_PROMISE) ?
+ frame->push_promise.promised_stream_id :
+ frame->hd.stream_id;
+}
+
+
+inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
+ const nghttp2_frame* frame,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ int32_t id = GetFrameID(frame);
+ DEBUG_HTTP2SESSION2(session, "beginning headers for stream %d", id);
+
+ Http2Stream* stream = session->FindStream(id);
+ if (stream == nullptr) {
+ new Http2Stream(session, id, frame->headers.cat);
+ } else {
+ stream->StartHeaders(frame->headers.cat);
+ }
+ return 0;
+}
+
+
+inline int Http2Session::OnHeaderCallback(nghttp2_session* handle,
+ const nghttp2_frame* frame,
+ nghttp2_rcbuf* name,
+ nghttp2_rcbuf* value,
+ uint8_t flags,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ int32_t id = GetFrameID(frame);
+ Http2Stream* stream = session->FindStream(id);
+ if (!stream->AddHeader(name, value, flags)) {
+ // This will only happen if the connected peer sends us more
+ // than the allowed number of header items at any given time
+ stream->SubmitRstStream(NGHTTP2_ENHANCE_YOUR_CALM);
+ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
+ }
+ return 0;
+}
+
+
+inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
+ const nghttp2_frame* frame,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ DEBUG_HTTP2SESSION2(session, "complete frame received: type: %d",
+ frame->hd.type);
+ switch (frame->hd.type) {
+ case NGHTTP2_DATA:
+ session->HandleDataFrame(frame);
+ break;
+ case NGHTTP2_PUSH_PROMISE:
+ // Intentional fall-through, handled just like headers frames
+ case NGHTTP2_HEADERS:
+ session->HandleHeadersFrame(frame);
+ break;
+ case NGHTTP2_SETTINGS:
+ session->HandleSettingsFrame(frame);
+ break;
+ case NGHTTP2_PRIORITY:
+ session->HandlePriorityFrame(frame);
+ break;
+ case NGHTTP2_GOAWAY:
+ session->HandleGoawayFrame(frame);
+ break;
+ case NGHTTP2_PING:
+ session->HandlePingFrame(frame);
+ default:
+ break;
+ }
+ return 0;
+}
+
+
+inline int Http2Session::OnFrameNotSent(nghttp2_session* handle,
+ const nghttp2_frame* frame,
+ int error_code,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ Environment* env = session->env();
+ DEBUG_HTTP2SESSION2(session, "frame type %d was not sent, code: %d",
+ frame->hd.type, error_code);
+ // Do not report if the frame was not sent due to the session closing
+ if (error_code != NGHTTP2_ERR_SESSION_CLOSING &&
+ error_code != NGHTTP2_ERR_STREAM_CLOSED &&
+ error_code != NGHTTP2_ERR_STREAM_CLOSING) {
+ Isolate* isolate = env->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env->context();
+ Context::Scope context_scope(context);
+
+ Local<Value> argv[3] = {
+ Integer::New(isolate, frame->hd.stream_id),
+ Integer::New(isolate, frame->hd.type),
+ Integer::New(isolate, error_code)
+ };
+ session->MakeCallback(env->onframeerror_string(), arraysize(argv), argv);
+ }
+ return 0;
+}
+
+
+inline int Http2Session::OnStreamClose(nghttp2_session* handle,
+ int32_t id,
+ uint32_t code,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ Environment* env = session->env();
+ Isolate* isolate = env->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env->context();
+ Context::Scope context_scope(context);
+ DEBUG_HTTP2SESSION2(session, "stream %d closed with code: %d", id, code);
+ Http2Stream* stream = session->FindStream(id);
+ // Intentionally ignore the callback if the stream does not exist
+ if (stream != nullptr) {
+ stream->Close(code);
+ // It is possible for the stream close to occur before the stream is
+ // ever passed on to the javascript side. If that happens, ignore this.
+ Local<Value> fn =
+ stream->object()->Get(context, env->onstreamclose_string())
+ .ToLocalChecked();
+ if (fn->IsFunction()) {
+ Local<Value> argv[1] = { Integer::NewFromUnsigned(isolate, code) };
+ stream->MakeCallback(fn.As<Function>(), arraysize(argv), argv);
+ }
+ }
+ return 0;
+}
+
+
+inline int Http2Session::OnInvalidHeader(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ nghttp2_rcbuf* name,
+ nghttp2_rcbuf* value,
+ uint8_t flags,
+ void* user_data) {
+ // Ignore invalid header fields by default.
+ return 0;
+}
+
+
+inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
+ uint8_t flags,
+ int32_t id,
+ const uint8_t* data,
+ size_t len,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ DEBUG_HTTP2SESSION2(session, "buffering data chunk for stream %d, size: "
+ "%d, flags: %d", id, len, flags);
+ // We should never actually get a 0-length chunk so this check is
+ // only a precaution at this point.
+ if (len > 0) {
+ CHECK_EQ(nghttp2_session_consume_connection(handle, len), 0);
+ Http2Stream* stream = session->FindStream(id);
+ stream->AddChunk(data, len);
+ }
+ return 0;
+}
+
+
+inline ssize_t Http2Session::OnSelectPadding(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ size_t maxPayloadLen,
+ void* user_data) {
+ Http2Session* handle = static_cast<Http2Session*>(user_data);
+ ssize_t padding = frame->hd.length;
+
+ return handle->padding_strategy_ == PADDING_STRATEGY_MAX
+ ? handle->OnMaxFrameSizePadding(padding, maxPayloadLen)
+ : handle->OnCallbackPadding(padding, maxPayloadLen);
+}
+
+#define BAD_PEER_MESSAGE "Remote peer returned unexpected data while we " \
+ "expected SETTINGS frame. Perhaps, peer does not " \
+ "support HTTP/2 properly."
+
+inline int Http2Session::OnNghttpError(nghttp2_session* handle,
+ const char* message,
+ size_t len,
+ void* user_data) {
+ // Unfortunately, this is currently the only way for us to know if
+ // the session errored because the peer is not an http2 peer.
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ DEBUG_HTTP2SESSION2(session, "Error '%.*s'", len, message);
+ if (strncmp(message, BAD_PEER_MESSAGE, len) == 0) {
+ Environment* env = session->env();
+ Isolate* isolate = env->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env->context();
+ Context::Scope context_scope(context);
+
+ Local<Value> argv[1] = {
+ Integer::New(isolate, NGHTTP2_ERR_PROTO),
+ };
+ session->MakeCallback(env->error_string(), arraysize(argv), argv);
+ }
+ return 0;
+}
+
+
+inline void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) {
+ if (stream->HasTrailers()) {
+ Http2Stream::SubmitTrailers submit_trailers{this, stream, flags};
+ stream->OnTrailers(submit_trailers);
+ }
+}
+
+
+Http2Stream::SubmitTrailers::SubmitTrailers(
+ Http2Session* session,
+ Http2Stream* stream,
+ uint32_t* flags)
+ : session_(session), stream_(stream), flags_(flags) { }
+
+
+inline void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers,
+ size_t length) const {
+ if (length == 0)
+ return;
+ DEBUG_HTTP2SESSION2(session_, "sending trailers for stream %d, count: %d",
+ stream_->id(), length);
+ *flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
+ CHECK_EQ(
+ nghttp2_submit_trailer(**session_, stream_->id(), trailers, length), 0);
+}
+
+
+inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env()->context();
+ Context::Scope context_scope(context);
+
+ int32_t id = GetFrameID(frame);
+ DEBUG_HTTP2SESSION2(this, "handle headers frame for stream %d", id);
+ Http2Stream* stream = FindStream(id);
+
+ nghttp2_header* headers = stream->headers();
+ size_t count = stream->headers_count();
+
+ Local<String> name_str;
+ Local<String> value_str;
+
+ Local<Array> holder = Array::New(isolate);
+ Local<Function> fn = env()->push_values_to_array_function();
+ Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2];
+
+ // The headers are passed in above as a queue of nghttp2_header structs.
+ // The following converts that into a JS array with the structure:
+ // [name1, value1, name2, value2, name3, value3, name3, value4] and so on.
+ // That array is passed up to the JS layer and converted into an Object form
+ // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it
+ // this way for performance reasons (it's faster to generate and pass an
+ // array than it is to generate and pass the object).
+ size_t n = 0;
+ while (count > 0) {
+ size_t j = 0;
+ while (count > 0 && j < arraysize(argv) / 2) {
+ nghttp2_header item = headers[n++];
+ // The header name and value are passed as external one-byte strings
+ name_str =
+ ExternalHeader::New<true>(env(), item.name).ToLocalChecked();
+ value_str =
+ ExternalHeader::New<false>(env(), item.value).ToLocalChecked();
+ argv[j * 2] = name_str;
+ argv[j * 2 + 1] = value_str;
+ count--;
+ j++;
+ }
+ // For performance, we pass name and value pairs to array.protototype.push
+ // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no
+ // more items to push.
+ if (j > 0) {
+ fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked();
+ }
+ }
+
+ Local<Value> args[5] = {
+ stream->object(),
+ Integer::New(isolate, id),
+ Integer::New(isolate, stream->headers_category()),
+ Integer::New(isolate, frame->hd.flags),
+ holder
+ };
+ MakeCallback(env()->onheaders_string(), arraysize(args), args);
+}
+
+
+inline void Http2Session::HandlePriorityFrame(const nghttp2_frame* frame) {
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env()->context();
+ Context::Scope context_scope(context);
+
+ nghttp2_priority priority_frame = frame->priority;
+ int32_t id = GetFrameID(frame);
+ DEBUG_HTTP2SESSION2(this, "handle priority frame for stream %d", id);
+ // Priority frame stream ID should never be <= 0. nghttp2 handles this for us
+ nghttp2_priority_spec spec = priority_frame.pri_spec;
+
+ Local<Value> argv[4] = {
+ Integer::New(isolate, id),
+ Integer::New(isolate, spec.stream_id),
+ Integer::New(isolate, spec.weight),
+ Boolean::New(isolate, spec.exclusive)
+ };
+ MakeCallback(env()->onpriority_string(), arraysize(argv), argv);
+}
+
+
+inline void Http2Session::HandleDataFrame(const nghttp2_frame* frame) {
+ int32_t id = GetFrameID(frame);
+ DEBUG_HTTP2SESSION2(this, "handling data frame for stream %d", id);
+ Http2Stream* stream = FindStream(id);
+
+ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
+ stream->AddChunk(nullptr, 0);
+ }
+
+ if (stream->IsReading())
+ stream->FlushDataChunks();
+}
+
+
+inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env()->context();
+ Context::Scope context_scope(context);
+
+ nghttp2_goaway goaway_frame = frame->goaway;
+ DEBUG_HTTP2SESSION(this, "handling goaway frame");
+
+ Local<Value> argv[3] = {
+ Integer::NewFromUnsigned(isolate, goaway_frame.error_code),
+ Integer::New(isolate, goaway_frame.last_stream_id),
+ Undefined(isolate)
+ };
+
+ size_t length = goaway_frame.opaque_data_len;
+ if (length > 0) {
+ argv[2] = Buffer::Copy(isolate,
+ reinterpret_cast<char*>(goaway_frame.opaque_data),
+ length).ToLocalChecked();
+ }
+
+ MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
+}
+
+inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
+ bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
+ if (ack) {
+ Http2Ping* ping = PopPing();
+ if (ping != nullptr)
+ ping->Done(true, frame->ping.opaque_data);
+ }
+}
+
+
+inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env()->context();
+ Context::Scope context_scope(context);
+
+ bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
+
+ Local<Value> argv[1] = { Boolean::New(isolate, ack) };
+ MakeCallback(env()->onsettings_string(), arraysize(argv), argv);
+}
+
+
+inline void Http2Session::SendPendingData() {
+ DEBUG_HTTP2SESSION(this, "sending pending data");
+ // Do not attempt to send data on the socket if the destroying flag has
+ // been set. That means everything is shutting down and the socket
+ // will not be usable.
+ if (IsDestroying())
+ return;
+
+ WriteWrap* req = nullptr;
+ char* dest = nullptr;
+ size_t destRemaining = 0;
+ size_t destLength = 0; // amount of data stored in dest
+ size_t destOffset = 0; // current write offset of dest
+
+ const uint8_t* src; // pointer to the serialized data
+ ssize_t srcLength = 0; // length of serialized data chunk
+
+ // While srcLength is greater than zero
+ while ((srcLength = nghttp2_session_mem_send(session_, &src)) > 0) {
+ if (req == nullptr) {
+ req = AllocateSend();
+ destRemaining = req->ExtraSize();
+ dest = req->Extra();
+ }
+ DEBUG_HTTP2SESSION2(this, "nghttp2 has %d bytes to send", srcLength);
+ size_t srcRemaining = srcLength;
+ size_t srcOffset = 0;
+
+ // The amount of data we have to copy is greater than the space
+ // remaining. Copy what we can into the remaining space, send it,
+ // the proceed with the rest.
+ while (srcRemaining > destRemaining) {
+ DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket",
+ destLength + destRemaining);
+ memcpy(dest + destOffset, src + srcOffset, destRemaining);
+ destLength += destRemaining;
+ Send(req, dest, destLength);
+ destOffset = 0;
+ destLength = 0;
+ srcRemaining -= destRemaining;
+ srcOffset += destRemaining;
+ req = AllocateSend();
+ destRemaining = req->ExtraSize();
+ dest = req->Extra();
+ }
+
+ if (srcRemaining > 0) {
+ memcpy(dest + destOffset, src + srcOffset, srcRemaining);
+ destLength += srcRemaining;
+ destOffset += srcRemaining;
+ destRemaining -= srcRemaining;
+ srcRemaining = 0;
+ srcOffset = 0;
+ }
+ }
+ CHECK_NE(srcLength, NGHTTP2_ERR_NOMEM);
+
+ if (destLength > 0) {
+ DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket", destLength);
+ Send(req, dest, destLength);
+ }
+}
+
+
+inline Http2Stream* Http2Session::SubmitRequest(
+ nghttp2_priority_spec* prispec,
+ nghttp2_nv* nva,
+ size_t len,
+ int32_t* ret,
+ int options) {
+ DEBUG_HTTP2SESSION(this, "submitting request");
+ Http2Stream* stream = nullptr;
+ Http2Stream::Provider::Stream prov(options);
+ *ret = nghttp2_submit_request(session_, prispec, nva, len, *prov, nullptr);
+ CHECK_NE(*ret, NGHTTP2_ERR_NOMEM);
+ if (*ret > 0)
+ stream = new Http2Stream(this, *ret, NGHTTP2_HCAT_HEADERS, options);
+ return stream;
+}
+
+inline void Http2Session::SetChunksSinceLastWrite(size_t n) {
+ chunks_sent_since_last_write_ = n;
+}
+
+
+WriteWrap* Http2Session::AllocateSend() {
+ HandleScope scope(env()->isolate());
+ auto AfterWrite = [](WriteWrap* req, int status) {
+ req->Dispose();
+ };
+ Local<Object> obj =
+ env()->write_wrap_constructor_function()
+ ->NewInstance(env()->context()).ToLocalChecked();
+ // Base the amount allocated on the remote peers max frame size
+ uint32_t size =
+ nghttp2_session_get_remote_settings(
+ session(),
+ NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
+ // Max frame size + 9 bytes for the header
+ return WriteWrap::New(env(), obj, stream_, AfterWrite, size + 9);
+}
+
+void Http2Session::Send(WriteWrap* req, char* buf, size_t length) {
+ DEBUG_HTTP2SESSION(this, "attempting to send data");
+ if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) {
+ return;
+ }
+
+ chunks_sent_since_last_write_++;
+ uv_buf_t actual = uv_buf_init(buf, length);
+ if (stream_->DoWrite(req, &actual, 1, nullptr)) {
+ req->Dispose();
+ }
+}
+
+
+void Http2Session::OnStreamAllocImpl(size_t suggested_size,
+ uv_buf_t* buf,
+ void* ctx) {
+ Http2Session* session = static_cast<Http2Session*>(ctx);
+ buf->base = session->stream_alloc();
+ buf->len = kAllocBufferSize;
+}
+
+
+void Http2Session::OnStreamReadImpl(ssize_t nread,
+ const uv_buf_t* bufs,
+ uv_handle_type pending,
+ void* ctx) {
+ Http2Session* session = static_cast<Http2Session*>(ctx);
+ if (nread < 0) {
+ uv_buf_t tmp_buf;
+ tmp_buf.base = nullptr;
+ tmp_buf.len = 0;
+ session->prev_read_cb_.fn(nread,
+ &tmp_buf,
+ pending,
+ session->prev_read_cb_.ctx);
+ return;
+ }
+ if (nread > 0) {
+ // Only pass data on if nread > 0
+ uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) };
+ ssize_t ret = session->Write(buf, 1);
+ if (ret < 0) {
+ DEBUG_HTTP2SESSION2(session, "fatal error receiving data: %d", ret);
+ CHECK_EQ(nghttp2_session_terminate_session(session->session(),
+ NGHTTP2_PROTOCOL_ERROR), 0);
+ }
+ }
+}
+
+
+void Http2Session::Consume(Local<External> external) {
+ StreamBase* stream = static_cast<StreamBase*>(external->Value());
+ stream->Consume();
+ stream_ = stream;
+ prev_alloc_cb_ = stream->alloc_cb();
+ prev_read_cb_ = stream->read_cb();
+ stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this });
+ stream->set_read_cb({ Http2Session::OnStreamReadImpl, this });
+ DEBUG_HTTP2SESSION(this, "i/o stream consumed");
+}
+
+
+void Http2Session::Unconsume() {
+ if (prev_alloc_cb_.is_empty())
+ return;
+ stream_->set_alloc_cb(prev_alloc_cb_);
+ stream_->set_read_cb(prev_read_cb_);
+ prev_alloc_cb_.clear();
+ prev_read_cb_.clear();
+ stream_ = nullptr;
+ DEBUG_HTTP2SESSION(this, "i/o stream unconsumed");
+}
+
+
+
+
+Http2Stream::Http2Stream(
+ Http2Session* session,
+ int32_t id,
+ nghttp2_headers_category category,
+ int options) : AsyncWrap(session->env(),
+ session->env()->http2stream_constructor_template()
+ ->NewInstance(session->env()->context())
+ .ToLocalChecked(),
+ AsyncWrap::PROVIDER_HTTP2STREAM),
+ StreamBase(session->env()),
+ session_(session),
+ id_(id),
+ current_headers_category_(category) {
+ MakeWeak<Http2Stream>(this);
+
+ // Limit the number of header pairs
+ max_header_pairs_ = session->GetMaxHeaderPairs();
+ if (max_header_pairs_ == 0)
+ max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
+ current_headers_.reserve(max_header_pairs_);
+
+ // Limit the number of header octets
+ max_header_length_ =
+ std::min(
+ nghttp2_session_get_local_settings(
+ session->session(),
+ NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE),
+ MAX_MAX_HEADER_LIST_SIZE);
+
+ if (options & STREAM_OPTION_GET_TRAILERS)
+ flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS;
+
+ if (options & STREAM_OPTION_EMPTY_PAYLOAD)
+ Shutdown();
+ session->AddStream(this);
+}
+
+
+Http2Stream::~Http2Stream() {
+ CHECK(persistent().IsEmpty());
+ if (!object().IsEmpty())
+ ClearWrap(object());
+ persistent().Reset();
+}
+
+void Http2Stream::StartHeaders(nghttp2_headers_category category) {
+ DEBUG_HTTP2STREAM2(this, "starting headers, category: %d", id_, category);
+ current_headers_length_ = 0;
+ current_headers_.clear();
+ current_headers_category_ = category;
+}
+
+nghttp2_stream* Http2Stream::operator*() {
+ return nghttp2_session_find_stream(**session_, id_);
+}
+
+
+void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) {
+ DEBUG_HTTP2STREAM(this, "prompting for trailers");
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Local<Context> context = env()->context();
+ Context::Scope context_scope(context);
+
+ Local<Value> ret =
+ MakeCallback(env()->ontrailers_string(), 0, nullptr).ToLocalChecked();
+ if (!ret.IsEmpty()) {
+ if (ret->IsArray()) {
+ Local<Array> headers = ret.As<Array>();
+ if (headers->Length() > 0) {
+ Headers trailers(isolate, context, headers);
+ submit_trailers.Submit(*trailers, trailers.length());
+ }
+ }
+ }
+}
+
+
+inline void Http2Stream::AddChunk(const uint8_t* data, size_t len) {
+ char* buf = nullptr;
+ if (len > 0) {
+ buf = Malloc<char>(len);
+ memcpy(buf, data, len);
+ }
+ data_chunks_.emplace(uv_buf_init(buf, len));
+}
+
+
+int Http2Stream::DoWrite(WriteWrap* req_wrap,
+ uv_buf_t* bufs,
+ size_t count,
+ uv_stream_t* send_handle) {
+ session_->SetChunksSinceLastWrite();
+
+ nghttp2_stream_write_t* req = new nghttp2_stream_write_t;
+ req->data = req_wrap;
+
+ auto AfterWrite = [](nghttp2_stream_write_t* req, int status) {
+ WriteWrap* wrap = static_cast<WriteWrap*>(req->data);
+ wrap->Done(status);
+ delete req;
+ };
+ req_wrap->Dispatched();
+ Write(req, bufs, count, AfterWrite);
+ return 0;
+}
+
+
+inline void Http2Stream::Close(int32_t code) {
+ flags_ |= NGHTTP2_STREAM_FLAG_CLOSED;
+ code_ = code;
+ DEBUG_HTTP2STREAM2(this, "closed with code %d", code);
+}
+
+
+inline void Http2Stream::Shutdown() {
+ flags_ |= NGHTTP2_STREAM_FLAG_SHUT;
+ CHECK_NE(nghttp2_session_resume_data(session_->session(), id_),
+ NGHTTP2_ERR_NOMEM);
+ DEBUG_HTTP2STREAM(this, "writable side shutdown");
+}
+
+int Http2Stream::DoShutdown(ShutdownWrap* req_wrap) {
+ req_wrap->Dispatched();
+ Shutdown();
+ req_wrap->Done(0);
+ return 0;
+}
+
+inline void Http2Stream::Destroy() {
+ DEBUG_HTTP2STREAM(this, "destroying stream");
+ // Do nothing if this stream instance is already destroyed
+ if (IsDestroyed())
+ return;
+
+ flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED;
+ Http2Session* session = this->session_;
+
+ if (session != nullptr) {
+ session_->RemoveStream(id_);
+ session_ = nullptr;
+ }
+
+ // Free any remaining incoming data chunks.
+ while (!data_chunks_.empty()) {
+ uv_buf_t buf = data_chunks_.front();
+ free(buf.base);
+ data_chunks_.pop();
+ }
+
+ // Free any remaining outgoing data chunks.
+ while (!queue_.empty()) {
+ nghttp2_stream_write* head = queue_.front();
+ head->cb(head->req, UV_ECANCELED);
+ delete head;
+ queue_.pop();
+ }
+
+ if (!object().IsEmpty())
+ ClearWrap(object());
+ persistent().Reset();
+
+ delete this;
+}
+
+
+void Http2Stream::OnDataChunk(
+ uv_buf_t* chunk) {
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ ssize_t len = -1;
+ Local<Object> buf;
+ if (chunk != nullptr) {
+ len = chunk->len;
+ buf = Buffer::New(isolate, chunk->base, len).ToLocalChecked();
+ }
+ EmitData(len, buf, this->object());
+}
+
+
+inline void Http2Stream::FlushDataChunks() {
+ if (!data_chunks_.empty()) {
+ uv_buf_t buf = data_chunks_.front();
+ data_chunks_.pop();
+ if (buf.len > 0) {
+ CHECK_EQ(nghttp2_session_consume_stream(session_->session(),
+ id_, buf.len), 0);
+ OnDataChunk(&buf);
+ } else {
+ OnDataChunk(nullptr);
+ }
+ }
+}
+
+
+inline int Http2Stream::SubmitResponse(nghttp2_nv* nva,
+ size_t len,
+ int options) {
+ DEBUG_HTTP2STREAM(this, "submitting response");
+ if (options & STREAM_OPTION_GET_TRAILERS)
+ flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS;
+
+ if (!IsWritable())
+ options |= STREAM_OPTION_EMPTY_PAYLOAD;
+
+ Http2Stream::Provider::Stream prov(this, options);
+ int ret = nghttp2_submit_response(session_->session(), id_, nva, len, *prov);
+ CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
+ return ret;
+}
+
+
+// Initiate a response that contains data read from a file descriptor.
+inline int Http2Stream::SubmitFile(int fd,
+ nghttp2_nv* nva, size_t len,
+ int64_t offset,
+ int64_t length,
+ int options) {
+ DEBUG_HTTP2STREAM(this, "submitting file");
+ if (options & STREAM_OPTION_GET_TRAILERS)
+ flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS;
+
+ if (offset > 0) fd_offset_ = offset;
+ if (length > -1) fd_length_ = length;
+
+ Http2Stream::Provider::FD prov(this, options, fd);
+ int ret = nghttp2_submit_response(session_->session(), id_, nva, len, *prov);
+ CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
+ return ret;
+}
+
+
+// Submit informational headers for a stream.
+inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
+ DEBUG_HTTP2STREAM2(this, "sending %d informational headers", len);
+ int ret = nghttp2_submit_headers(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_, nullptr,
+ nva, len, nullptr);
+ CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
+ return ret;
+}
+
+
+inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
+ bool silent) {
+ DEBUG_HTTP2STREAM(this, "sending priority spec");
+ int ret = silent ?
+ nghttp2_session_change_stream_priority(session_->session(),
+ id_, prispec) :
+ nghttp2_submit_priority(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_, prispec);
+ CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
+ return ret;
+}
+
+
+inline int Http2Stream::SubmitRstStream(const uint32_t code) {
+ DEBUG_HTTP2STREAM2(this, "sending rst-stream with code %d", code);
+ session_->SendPendingData();
+ CHECK_EQ(nghttp2_submit_rst_stream(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_,
+ code), 0);
+ return 0;
+}
+
+
+// Submit a push promise.
+inline Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva,
+ size_t len,
+ int32_t* ret,
+ int options) {
+ DEBUG_HTTP2STREAM(this, "sending push promise");
+ *ret = nghttp2_submit_push_promise(session_->session(), NGHTTP2_FLAG_NONE,
+ id_, nva, len, nullptr);
+ CHECK_NE(*ret, NGHTTP2_ERR_NOMEM);
+ Http2Stream* stream = nullptr;
+ if (*ret > 0)
+ stream = new Http2Stream(session_, *ret, NGHTTP2_HCAT_HEADERS, options);
+
+ return stream;
+}
+
+inline int Http2Stream::ReadStart() {
+ flags_ |= NGHTTP2_STREAM_FLAG_READ_START;
+ flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED;
+
+ // Flush any queued data chunks immediately out to the JS layer
+ FlushDataChunks();
+ DEBUG_HTTP2STREAM(this, "reading starting");
+ return 0;
+}
+
+
+inline int Http2Stream::ReadStop() {
+ if (!IsReading())
+ return 0;
+ flags_ |= NGHTTP2_STREAM_FLAG_READ_PAUSED;
+ DEBUG_HTTP2STREAM(this, "reading stopped");
+ return 0;
+}
+
+// Queue the given set of uv_but_t handles for writing to an
+// nghttp2_stream. The callback will be invoked once the chunks
+// of data have been flushed to the underlying nghttp2_session.
+// Note that this does *not* mean that the data has been flushed
+// to the socket yet.
+inline int Http2Stream::Write(nghttp2_stream_write_t* req,
+ const uv_buf_t bufs[],
+ unsigned int nbufs,
+ nghttp2_stream_write_cb cb) {
+ if (!IsWritable()) {
+ if (cb != nullptr)
+ cb(req, UV_EOF);
+ return 0;
+ }
+ DEBUG_HTTP2STREAM2(this, "queuing %d buffers to send", id_, nbufs);
+ nghttp2_stream_write* item = new nghttp2_stream_write;
+ item->cb = cb;
+ item->req = req;
+ item->nbufs = nbufs;
+ item->bufs.AllocateSufficientStorage(nbufs);
+ memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs));
+ queue_.push(item);
+ CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM);
+ return 0;
+}
+
+inline size_t GetBufferLength(nghttp2_rcbuf* buf) {
+ return nghttp2_rcbuf_get_buf(buf).len;
+}
+
+inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
+ nghttp2_rcbuf* value,
+ uint8_t flags) {
+ size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
+ if (current_headers_.size() == max_header_pairs_ ||
+ current_headers_length_ + length > max_header_length_) {
+ return false;
+ }
+ nghttp2_header header;
+ header.name = name;
+ header.value = value;
+ header.flags = flags;
+ current_headers_.push_back(header);
+ nghttp2_rcbuf_incref(name);
+ nghttp2_rcbuf_incref(value);
+ current_headers_length_ += length;
+ return true;
+}
+
+
+Http2Stream* GetStream(Http2Session* session,
+ int32_t id,
+ nghttp2_data_source* source) {
+ Http2Stream* stream = static_cast<Http2Stream*>(source->ptr);
+ if (stream == nullptr)
+ stream = session->FindStream(id);
+ CHECK_NE(stream, nullptr);
+ CHECK_EQ(id, stream->id());
+ return stream;
+}
+
+Http2Stream::Provider::Provider(Http2Stream* stream, int options) {
+ provider_.source.ptr = stream;
+ empty_ = options & STREAM_OPTION_EMPTY_PAYLOAD;
+}
+
+Http2Stream::Provider::Provider(int options) {
+ provider_.source.ptr = nullptr;
+ empty_ = options & STREAM_OPTION_EMPTY_PAYLOAD;
+}
+
+Http2Stream::Provider::~Provider() {
+ provider_.source.ptr = nullptr;
+}
+
+Http2Stream::Provider::FD::FD(Http2Stream* stream, int options, int fd)
+ : Http2Stream::Provider(stream, options) {
+ provider_.source.fd = fd;
+ provider_.read_callback = Http2Stream::Provider::FD::OnRead;
+}
+
+Http2Stream::Provider::FD::FD(int options, int fd)
+ : Http2Stream::Provider(options) {
+ provider_.source.fd = fd;
+ provider_.read_callback = Http2Stream::Provider::FD::OnRead;
+}
+
+ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ Http2Stream* stream = session->FindStream(id);
+ DEBUG_HTTP2SESSION2(session, "reading outbound file data for stream %d", id);
+ CHECK_EQ(id, stream->id());
+
+ int fd = source->fd;
+ int64_t offset = stream->fd_offset_;
+ ssize_t numchars = 0;
+
+ if (stream->fd_length_ >= 0 &&
+ stream->fd_length_ < static_cast<int64_t>(length))
+ length = stream->fd_length_;
+
+ uv_buf_t data;
+ data.base = reinterpret_cast<char*>(buf);
+ data.len = length;
+
+ uv_fs_t read_req;
+
+ if (length > 0) {
+ // TODO(addaleax): Never use synchronous I/O on the main thread.
+ numchars = uv_fs_read(session->event_loop(),
+ &read_req,
+ fd, &data, 1,
+ offset, nullptr);
+ uv_fs_req_cleanup(&read_req);
+ }
+
+ // Close the stream with an error if reading fails
+ if (numchars < 0)
+ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
+
+ // Update the read offset for the next read
+ stream->fd_offset_ += numchars;
+ stream->fd_length_ -= numchars;
+
+ // if numchars < length, assume that we are done.
+ if (static_cast<size_t>(numchars) < length || length <= 0) {
+ DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id);
+ *flags |= NGHTTP2_DATA_FLAG_EOF;
+ session->GetTrailers(stream, flags);
+ }
+
+ return numchars;
+}
+
+Http2Stream::Provider::Stream::Stream(int options)
+ : Http2Stream::Provider(options) {
+ provider_.read_callback = Http2Stream::Provider::Stream::OnRead;
+}
+
+Http2Stream::Provider::Stream::Stream(Http2Stream* stream, int options)
+ : Http2Stream::Provider(stream, options) {
+ provider_.read_callback = Http2Stream::Provider::Stream::OnRead;
+}
+
+ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data) {
+ Http2Session* session = static_cast<Http2Session*>(user_data);
+ DEBUG_HTTP2SESSION2(session, "reading outbound data for stream %d", id);
+ Http2Stream* stream = GetStream(session, id, source);
+ CHECK_EQ(id, stream->id());
+
+ size_t amount = 0; // amount of data being sent in this data frame.
+
+ uv_buf_t current;
+
+ if (!stream->queue_.empty()) {
+ DEBUG_HTTP2SESSION2(session, "stream %d has pending outbound data", id);
+ nghttp2_stream_write* head = stream->queue_.front();
+ current = head->bufs[stream->queue_index_];
+ size_t clen = current.len - stream->queue_offset_;
+ amount = std::min(clen, length);
+ DEBUG_HTTP2SESSION2(session, "sending %d bytes for data frame on stream %d",
+ amount, id);
+ if (amount > 0) {
+ memcpy(buf, current.base + stream->queue_offset_, amount);
+ stream->queue_offset_ += amount;
+ }
+ if (stream->queue_offset_ == current.len) {
+ stream->queue_index_++;
+ stream->queue_offset_ = 0;
+ }
+ if (stream->queue_index_ == head->nbufs) {
+ head->cb(head->req, 0);
+ delete head;
+ stream->queue_.pop();
+ stream->queue_offset_ = 0;
+ stream->queue_index_ = 0;
+ }
+ }
+
+ if (amount == 0 && stream->IsWritable() && stream->queue_.empty()) {
+ DEBUG_HTTP2SESSION2(session, "deferring stream %d", id);
+ return NGHTTP2_ERR_DEFERRED;
+ }
+
+ if (stream->queue_.empty() && !stream->IsWritable()) {
+ DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id);
+ *flags |= NGHTTP2_DATA_FLAG_EOF;
+
+ session->GetTrailers(stream, flags);
+ }
+
+ return amount;
}
+
+
+// Implementation of the JavaScript API
+
void HttpErrorString(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
uint32_t val = args[0]->Uint32Value(env->context()).ToChecked();
args.GetReturnValue().Set(
- OneByteString(env->isolate(), nghttp2_strerror(val)));
+ String::NewFromOneByte(
+ env->isolate(),
+ reinterpret_cast<const uint8_t*>(nghttp2_strerror(val)),
+ v8::NewStringType::kInternalized).ToLocalChecked());
}
+
// Serializes the settings object into a Buffer instance that
// would be suitable, for instance, for creating the Base64
// output for an HTTP2-Settings header field.
@@ -310,35 +1601,47 @@ void PackSettings(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(settings.Pack());
}
-// Used to fill in the spec defined initial values for each setting.
+
void RefreshDefaultSettings(const FunctionCallbackInfo<Value>& args) {
- DEBUG_HTTP2("Http2Session: refreshing default settings\n");
Environment* env = Environment::GetCurrent(args);
Http2Settings::RefreshDefaults(env);
}
+
+void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+ if (nghttp2_session_set_next_stream_id(**session, id) < 0) {
+ DEBUG_HTTP2SESSION2(session, "failed to set next stream id to %d", id);
+ return args.GetReturnValue().Set(false);
+ }
+ args.GetReturnValue().Set(true);
+ DEBUG_HTTP2SESSION2(session, "set next stream id to %d", id);
+}
+
+
template <get_setting fn>
void Http2Session::RefreshSettings(const FunctionCallbackInfo<Value>& args) {
- DEBUG_HTTP2("Http2Session: refreshing settings for session\n");
Environment* env = Environment::GetCurrent(args);
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Http2Settings::Update(env, session, fn);
+ DEBUG_HTTP2SESSION(session, "settings refreshed for session");
}
-// Used to fill in the spec defined initial values for each setting.
-void RefreshSessionState(const FunctionCallbackInfo<Value>& args) {
- DEBUG_HTTP2("Http2Session: refreshing session state\n");
+
+void Http2Session::RefreshState(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
-#if defined(DEBUG) && DEBUG
- CHECK_EQ(args.Length(), 1);
- CHECK(args[0]->IsObject());
-#endif
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ DEBUG_HTTP2SESSION(session, "refreshing state");
+
AliasedBuffer<double, v8::Float64Array>& buffer =
env->http2_state()->session_state_buffer;
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>());
- nghttp2_session* s = session->session();
+
+ nghttp2_session* s = **session;
buffer[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE] =
nghttp2_session_get_effective_local_window_size(s);
@@ -360,84 +1663,31 @@ void RefreshSessionState(const FunctionCallbackInfo<Value>& args) {
nghttp2_session_get_hd_inflate_dynamic_table_size(s);
}
-void RefreshStreamState(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
-#if defined(DEBUG) && DEBUG
- CHECK_EQ(args.Length(), 2);
- CHECK(args[0]->IsObject());
- CHECK(args[1]->IsNumber());
-#endif
- int32_t id = args[1]->Int32Value(env->context()).ToChecked();
- DEBUG_HTTP2("Http2Session: refreshing stream %d state\n", id);
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>());
- nghttp2_session* s = session->session();
- Nghttp2Stream* stream;
-
- AliasedBuffer<double, v8::Float64Array>& buffer =
- env->http2_state()->stream_state_buffer;
-
- if ((stream = session->FindStream(id)) == nullptr) {
- buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE;
- buffer[IDX_STREAM_STATE_WEIGHT] =
- buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
- buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
- buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
- buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0;
- return;
- }
- nghttp2_stream* str =
- nghttp2_session_find_stream(s, stream->id());
-
- if (str == nullptr) {
- buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE;
- buffer[IDX_STREAM_STATE_WEIGHT] =
- buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
- buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
- buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
- buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0;
- } else {
- buffer[IDX_STREAM_STATE] =
- nghttp2_stream_get_state(str);
- buffer[IDX_STREAM_STATE_WEIGHT] =
- nghttp2_stream_get_weight(str);
- buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
- nghttp2_stream_get_sum_dependency_weight(str);
- buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
- nghttp2_session_get_stream_local_close(s, id);
- buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
- nghttp2_session_get_stream_remote_close(s, id);
- buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] =
- nghttp2_session_get_stream_local_window_size(s, id);
- }
-}
void Http2Session::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
-#if defined(DEBUG) && DEBUG
CHECK(args.IsConstructCall());
-#endif
int val = args[0]->IntegerValue(env->context()).ToChecked();
nghttp2_session_type type = static_cast<nghttp2_session_type>(val);
- DEBUG_HTTP2("Http2Session: creating a session of type: %d\n", type);
- new Http2Session(env, args.This(), type);
+ Http2Session* session = new Http2Session(env, args.This(), type);
+ session->get_async_id(); // avoid compiler warning
+ DEBUG_HTTP2SESSION(session, "session created");
}
-// Capture the stream that this session will use to send and receive data
void Http2Session::Consume(const FunctionCallbackInfo<Value>& args) {
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-#if defined(DEBUG) && DEBUG
CHECK(args[0]->IsExternal());
-#endif
session->Consume(args[0].As<External>());
}
+
void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) {
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- DEBUG_HTTP2("Http2Session: destroying session %d\n", session->type());
+ DEBUG_HTTP2SESSION(session, "destroying session");
+
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
@@ -448,79 +1698,27 @@ void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) {
session->Close();
}
+
void Http2Session::Destroying(const FunctionCallbackInfo<Value>& args) {
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- DEBUG_HTTP2("Http2Session: preparing to destroy session %d\n",
- session->type());
session->MarkDestroying();
+ DEBUG_HTTP2SESSION(session, "preparing to destroy session");
}
-void Http2Session::SubmitPriority(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- Local<Context> context = env->context();
-
- int32_t id = args[0]->Int32Value(context).ToChecked();
- Http2Priority priority(env, args[1], args[2], args[3]);
- bool silent = args[4]->BooleanValue(context).ToChecked();
- DEBUG_HTTP2("Http2Session: submitting priority for stream %d", id);
-
- Nghttp2Stream* stream;
- if (!(stream = session->FindStream(id))) {
- // invalid stream
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
-
- args.GetReturnValue().Set(stream->SubmitPriority(*priority, silent));
-}
-void Http2Session::SubmitSettings(const FunctionCallbackInfo<Value>& args) {
+void Http2Session::Settings(const FunctionCallbackInfo<Value>& args) {
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Environment* env = session->env();
Http2Settings settings(env);
- args.GetReturnValue().Set(
- session->Nghttp2Session::SubmitSettings(*settings, settings.length()));
+ session->Http2Session::Settings(*settings, settings.length());
+ DEBUG_HTTP2SESSION(session, "settings submitted");
}
-void Http2Session::SubmitRstStream(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
- Local<Context> context = env->context();
-
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber());
- CHECK(args[1]->IsNumber());
-#endif
-
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-
- int32_t id = args[0]->Int32Value(context).ToChecked();
- uint32_t code = args[1]->Uint32Value(context).ToChecked();
-
- Nghttp2Stream* stream;
- if (!(stream = session->FindStream(id))) {
- // invalid stream
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
- DEBUG_HTTP2("Http2Session: sending rst_stream for stream %d, code: %d\n",
- id, code);
- args.GetReturnValue().Set(stream->SubmitRstStream(code));
-}
-
-void Http2Session::SubmitRequest(const FunctionCallbackInfo<Value>& args) {
- // args[0] Array of headers
- // args[1] options int
- // args[2] parentStream ID (for priority spec)
- // args[3] weight (for priority spec)
- // args[4] exclusive boolean (for priority spec)
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsArray());
-#endif
+void Http2Session::Request(const FunctionCallbackInfo<Value>& args) {
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Environment* env = session->env();
@@ -531,177 +1729,36 @@ void Http2Session::SubmitRequest(const FunctionCallbackInfo<Value>& args) {
int options = args[1]->IntegerValue(context).ToChecked();
Http2Priority priority(env, args[2], args[3], args[4]);
- DEBUG_HTTP2("Http2Session: submitting request: headers: %d, options: %d\n",
- headers->Length(), options);
-
Headers list(isolate, context, headers);
- int32_t ret = session->Nghttp2Session::SubmitRequest(*priority,
- *list, list.length(),
- nullptr, options);
- DEBUG_HTTP2("Http2Session: request submitted, response: %d\n", ret);
- args.GetReturnValue().Set(ret);
-}
+ DEBUG_HTTP2SESSION(session, "request submitted");
-void Http2Session::SubmitResponse(const FunctionCallbackInfo<Value>& args) {
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber());
- CHECK(args[1]->IsArray());
-#endif
-
- Http2Session* session;
- Nghttp2Stream* stream;
+ int32_t ret = 0;
+ Http2Stream* stream =
+ session->Http2Session::SubmitRequest(*priority, *list, list.length(),
+ &ret, options);
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- Environment* env = session->env();
- Local<Context> context = env->context();
- Isolate* isolate = env->isolate();
-
- int32_t id = args[0]->Int32Value(context).ToChecked();
- Local<Array> headers = args[1].As<Array>();
- int options = args[2]->IntegerValue(context).ToChecked();
-
- DEBUG_HTTP2("Http2Session: submitting response for stream %d: headers: %d, "
- "options: %d\n", id, headers->Length(), options);
-
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ if (ret <= 0) {
+ DEBUG_HTTP2SESSION2(session, "could not submit request: %s",
+ nghttp2_strerror(ret));
+ return args.GetReturnValue().Set(ret);
}
- Headers list(isolate, context, headers);
-
- args.GetReturnValue().Set(
- stream->SubmitResponse(*list, list.length(), options));
+ DEBUG_HTTP2SESSION2(session, "request submitted, new stream id %d",
+ stream->id());
+ args.GetReturnValue().Set(stream->object());
}
-void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) {
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber()); // Stream ID
- CHECK(args[1]->IsNumber()); // File Descriptor
- CHECK(args[2]->IsArray()); // Headers
- CHECK(args[3]->IsNumber()); // Offset
- CHECK(args[4]->IsNumber()); // Length
-#endif
- Http2Session* session;
- Nghttp2Stream* stream;
-
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- Environment* env = session->env();
- Local<Context> context = env->context();
- Isolate* isolate = env->isolate();
-
- int32_t id = args[0]->Int32Value(context).ToChecked();
- int fd = args[1]->Int32Value(context).ToChecked();
- Local<Array> headers = args[2].As<Array>();
-
- int64_t offset = args[3]->IntegerValue(context).ToChecked();
- int64_t length = args[4]->IntegerValue(context).ToChecked();
- int options = args[5]->IntegerValue(context).ToChecked();
-
-#if defined(DEBUG) && DEBUG
- CHECK_GE(offset, 0);
-#endif
-
- DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, "
- "end-stream: %d\n", fd, id, headers->Length());
-
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
-
- session->chunks_sent_since_last_write_ = 0;
-
- Headers list(isolate, context, headers);
-
- args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length(),
- offset, length, options));
-}
-
-void Http2Session::SendHeaders(const FunctionCallbackInfo<Value>& args) {
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber());
- CHECK(args[1]->IsArray());
-#endif
-
- Http2Session* session;
- Nghttp2Stream* stream;
-
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- Environment* env = session->env();
- Local<Context> context = env->context();
- Isolate* isolate = env->isolate();
-
- int32_t id = args[0]->Int32Value(env->context()).ToChecked();
- Local<Array> headers = args[1].As<Array>();
-
- DEBUG_HTTP2("Http2Session: sending informational headers for stream %d, "
- "count: %d\n", id, headers->Length());
-
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
-
- Headers list(isolate, context, headers);
-
- args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length()));
-}
-
-void Http2Session::ShutdownStream(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber());
-#endif
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- Nghttp2Stream* stream;
- int32_t id = args[0]->Int32Value(env->context()).ToChecked();
- DEBUG_HTTP2("Http2Session: shutting down stream %d\n", id);
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
- stream->Shutdown();
-}
-
-void Http2Session::StreamReadStart(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber());
-#endif
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- Nghttp2Stream* stream;
- int32_t id = args[0]->Int32Value(env->context()).ToChecked();
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
- stream->ReadStart();
-}
-
-
-void Http2Session::StreamReadStop(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber());
-#endif
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
- Nghttp2Stream* stream;
- int32_t id = args[0]->Int32Value(env->context()).ToChecked();
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
- stream->ReadStop();
-}
-
-void Http2Session::SendShutdownNotice(
- const FunctionCallbackInfo<Value>& args) {
+void Http2Session::ShutdownNotice(const FunctionCallbackInfo<Value>& args) {
Http2Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
session->SubmitShutdownNotice();
+ DEBUG_HTTP2SESSION(session, "shutdown notice sent");
}
-void Http2Session::SubmitGoaway(const FunctionCallbackInfo<Value>& args) {
+
+void Http2Session::Goaway(const FunctionCallbackInfo<Value>& args) {
Http2Session* session;
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
@@ -721,58 +1778,25 @@ void Http2Session::SubmitGoaway(const FunctionCallbackInfo<Value>& args) {
length = buf_length;
}
- DEBUG_HTTP2("Http2Session: initiating immediate shutdown. "
- "last-stream-id: %d, code: %d, opaque-data: %d\n",
- lastStreamID, errorCode, length);
int status = nghttp2_submit_goaway(session->session(),
NGHTTP2_FLAG_NONE,
lastStreamID,
errorCode,
data, length);
+ CHECK_NE(status, NGHTTP2_ERR_NOMEM);
args.GetReturnValue().Set(status);
+ DEBUG_HTTP2SESSION2(session, "immediate shutdown initiated with "
+ "last stream id %d, code %d, and opaque-data length %d",
+ lastStreamID, errorCode, length);
}
-void Http2Session::DestroyStream(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-#if defined(DEBUG) && DEBUG
- CHECK_EQ(args.Length(), 1);
- CHECK(args[0]->IsNumber());
-#endif
- int32_t id = args[0]->Int32Value(env->context()).ToChecked();
- DEBUG_HTTP2("Http2Session: destroy stream %d\n", id);
- Nghttp2Stream* stream;
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
- stream->Destroy();
-}
-
-void Http2Session::FlushData(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
- Http2Session* session;
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-#if defined(DEBUG) && DEBUG
- CHECK_EQ(args.Length(), 1);
- CHECK(args[0]->IsNumber());
-#endif
- int32_t id = args[0]->Int32Value(env->context()).ToChecked();
- DEBUG_HTTP2("Http2Session: flushing data to js for stream %d\n", id);
- Nghttp2Stream* stream;
- if (!(stream = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
- stream->ReadResume();
-}
void Http2Session::UpdateChunksSent(const FunctionCallbackInfo<Value>& args) {
- Http2Session* session;
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-
HandleScope scope(isolate);
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
uint32_t length = session->chunks_sent_since_last_write_;
@@ -783,426 +1807,260 @@ void Http2Session::UpdateChunksSent(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(length);
}
-void Http2Session::SubmitPushPromise(const FunctionCallbackInfo<Value>& args) {
- Http2Session* session;
+
+void Http2Stream::RstStream(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
- Isolate* isolate = env->isolate();
- ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
+ uint32_t code = args[0]->Uint32Value(context).ToChecked();
+ args.GetReturnValue().Set(stream->SubmitRstStream(code));
+ DEBUG_HTTP2STREAM2(stream, "rst_stream code %d sent", code);
+}
-#if defined(DEBUG) && DEBUG
- CHECK(args[0]->IsNumber()); // parent stream ID
- CHECK(args[1]->IsArray()); // headers array
-#endif
- Nghttp2Stream* parent;
- int32_t id = args[0]->Int32Value(context).ToChecked();
- Local<Array> headers = args[1].As<Array>();
- int options = args[2]->IntegerValue(context).ToChecked();
-
- DEBUG_HTTP2("Http2Session: submitting push promise for stream %d: "
- "options: %d, headers: %d\n", id, options,
- headers->Length());
+void Http2Stream::Respond(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
- if (!(parent = session->FindStream(id))) {
- return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
- }
+ Local<Array> headers = args[0].As<Array>();
+ int options = args[1]->IntegerValue(context).ToChecked();
Headers list(isolate, context, headers);
- int32_t ret = parent->SubmitPushPromise(*list, list.length(),
- nullptr, options);
- DEBUG_HTTP2("Http2Session: push promise submitted, ret: %d\n", ret);
- args.GetReturnValue().Set(ret);
+ args.GetReturnValue().Set(
+ stream->SubmitResponse(*list, list.length(), options));
+ DEBUG_HTTP2STREAM(stream, "response submitted");
}
-int Http2Session::DoWrite(WriteWrap* req_wrap,
- uv_buf_t* bufs,
- size_t count,
- uv_stream_t* send_handle) {
- Environment* env = req_wrap->env();
- Local<Object> req_wrap_obj = req_wrap->object();
+
+void Http2Stream::RespondFD(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
- Nghttp2Stream* stream;
- {
- Local<Value> val =
- req_wrap_obj->Get(context, env->stream_string()).ToLocalChecked();
- int32_t id = val->Int32Value(context).ToChecked();
- if (!val->IsNumber() || !(stream = FindStream(id))) {
- // invalid stream
- req_wrap->Dispatched();
- req_wrap->Done(0);
- return NGHTTP2_ERR_INVALID_STREAM_ID;
- }
- }
+ int fd = args[0]->Int32Value(context).ToChecked();
+ Local<Array> headers = args[1].As<Array>();
- chunks_sent_since_last_write_ = 0;
+ int64_t offset = args[2]->IntegerValue(context).ToChecked();
+ int64_t length = args[3]->IntegerValue(context).ToChecked();
+ int options = args[4]->IntegerValue(context).ToChecked();
- nghttp2_stream_write_t* req = new nghttp2_stream_write_t;
- req->data = req_wrap;
+ stream->session()->SetChunksSinceLastWrite();
- auto AfterWrite = [](nghttp2_stream_write_t* req, int status) {
- WriteWrap* wrap = static_cast<WriteWrap*>(req->data);
- wrap->Done(status);
- delete req;
- };
- req_wrap->Dispatched();
- stream->Write(req, bufs, count, AfterWrite);
- return 0;
+ Headers list(isolate, context, headers);
+ args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length(),
+ offset, length, options));
+ DEBUG_HTTP2STREAM2(stream, "file response submitted for fd %d", fd);
}
-WriteWrap* Http2Session::AllocateSend() {
- HandleScope scope(env()->isolate());
- auto AfterWrite = [](WriteWrap* req, int status) {
- req->Dispose();
- };
- Local<Object> obj =
- env()->write_wrap_constructor_function()
- ->NewInstance(env()->context()).ToLocalChecked();
- // Base the amount allocated on the remote peers max frame size
- uint32_t size =
- nghttp2_session_get_remote_settings(
- session(),
- NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
- // Max frame size + 9 bytes for the header
- return WriteWrap::New(env(), obj, this, AfterWrite, size + 9);
-}
-void Http2Session::Send(WriteWrap* req, char* buf, size_t length) {
- DEBUG_HTTP2("Http2Session: Attempting to send data\n");
- if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) {
- return;
- }
+void Http2Stream::Info(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
- chunks_sent_since_last_write_++;
- uv_buf_t actual = uv_buf_init(buf, length);
- if (stream_->DoWrite(req, &actual, 1, nullptr)) {
- req->Dispose();
- }
-}
+ Local<Array> headers = args[0].As<Array>();
-void Http2Session::OnTrailers(Nghttp2Stream* stream,
- const SubmitTrailers& submit_trailers) {
- DEBUG_HTTP2("Http2Session: prompting for trailers on stream %d\n",
- stream->id());
- Local<Context> context = env()->context();
- Isolate* isolate = env()->isolate();
- HandleScope scope(isolate);
- Context::Scope context_scope(context);
+ Headers list(isolate, context, headers);
+ args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length()));
+ DEBUG_HTTP2STREAM2(stream, "%d informational headers sent",
+ headers->Length());
+}
- Local<Value> argv[1] = {
- Integer::New(isolate, stream->id())
- };
- Local<Value> ret = MakeCallback(env()->ontrailers_string(),
- arraysize(argv), argv).ToLocalChecked();
- if (!ret.IsEmpty()) {
- if (ret->IsArray()) {
- Local<Array> headers = ret.As<Array>();
- if (headers->Length() > 0) {
- Headers trailers(isolate, context, headers);
- submit_trailers.Submit(*trailers, trailers.length());
- }
- }
- }
+void Http2Stream::GetID(const FunctionCallbackInfo<Value>& args) {
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
+ args.GetReturnValue().Set(stream->id());
}
-void Http2Session::OnHeaders(
- Nghttp2Stream* stream,
- nghttp2_header* headers,
- size_t count,
- nghttp2_headers_category cat,
- uint8_t flags) {
- Local<Context> context = env()->context();
- Isolate* isolate = env()->isolate();
- Context::Scope context_scope(context);
- HandleScope scope(isolate);
- Local<String> name_str;
- Local<String> value_str;
- Local<Array> holder = Array::New(isolate);
- Local<Function> fn = env()->push_values_to_array_function();
- Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2];
-
-#if defined(DEBUG) && DEBUG
- CHECK_LE(cat, NGHTTP2_HCAT_HEADERS);
-#endif
+void Http2Stream::Destroy(const FunctionCallbackInfo<Value>& args) {
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
+ DEBUG_HTTP2STREAM(stream, "destroying stream");
+ stream->Destroy();
+}
- // The headers are passed in above as a queue of nghttp2_header structs.
- // The following converts that into a JS array with the structure:
- // [name1, value1, name2, value2, name3, value3, name3, value4] and so on.
- // That array is passed up to the JS layer and converted into an Object form
- // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it
- // this way for performance reasons (it's faster to generate and pass an
- // array than it is to generate and pass the object).
- size_t n = 0;
- while (count > 0) {
- size_t j = 0;
- while (count > 0 && j < arraysize(argv) / 2) {
- nghttp2_header item = headers[n++];
- // The header name and value are passed as external one-byte strings
- name_str =
- ExternalHeader::New<true>(env(), item.name).ToLocalChecked();
- value_str =
- ExternalHeader::New<false>(env(), item.value).ToLocalChecked();
- argv[j * 2] = name_str;
- argv[j * 2 + 1] = value_str;
- count--;
- j++;
- }
- // For performance, we pass name and value pairs to array.protototype.push
- // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no
- // more items to push.
- if (j > 0) {
- fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked();
- }
- }
- Local<Value> args[4] = {
- Integer::New(isolate, stream->id()),
- Integer::New(isolate, cat),
- Integer::New(isolate, flags),
- holder
- };
- MakeCallback(env()->onheaders_string(), arraysize(args), args);
+void Http2Stream::FlushData(const FunctionCallbackInfo<Value>& args) {
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
+ stream->ReadStart();
+ DEBUG_HTTP2STREAM(stream, "data flushed to js");
}
-void Http2Session::OnStreamClose(int32_t id, uint32_t code) {
- Isolate* isolate = env()->isolate();
- Local<Context> context = env()->context();
- HandleScope scope(isolate);
- Context::Scope context_scope(context);
+void Http2Stream::PushPromise(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+ Http2Stream* parent;
+ ASSIGN_OR_RETURN_UNWRAP(&parent, args.Holder());
- Local<Value> argv[2] = {
- Integer::New(isolate, id),
- Integer::NewFromUnsigned(isolate, code)
- };
- MakeCallback(env()->onstreamclose_string(), arraysize(argv), argv);
-}
+ Local<Array> headers = args[0].As<Array>();
+ int options = args[1]->IntegerValue(context).ToChecked();
-void Http2Session::OnDataChunk(
- Nghttp2Stream* stream,
- uv_buf_t* chunk) {
- Isolate* isolate = env()->isolate();
- Local<Context> context = env()->context();
- HandleScope scope(isolate);
- Local<Object> obj = Object::New(isolate);
- obj->Set(context,
- env()->id_string(),
- Integer::New(isolate, stream->id())).FromJust();
- ssize_t len = -1;
- Local<Object> buf;
- if (chunk != nullptr) {
- len = chunk->len;
- buf = Buffer::New(isolate, chunk->base, len).ToLocalChecked();
- }
- EmitData(len, buf, obj);
-}
+ Headers list(isolate, context, headers);
-void Http2Session::OnSettings(bool ack) {
- Local<Context> context = env()->context();
- Isolate* isolate = env()->isolate();
- HandleScope scope(isolate);
- Context::Scope context_scope(context);
+ DEBUG_HTTP2STREAM(parent, "creating push promise");
- Local<Value> argv[1] = { Boolean::New(isolate, ack) };
- MakeCallback(env()->onsettings_string(), arraysize(argv), argv);
+ int32_t ret = 0;
+ Http2Stream* stream = parent->SubmitPushPromise(*list, list.length(),
+ &ret, options);
+ if (ret <= 0) {
+ DEBUG_HTTP2STREAM2(parent, "failed to create push stream: %d", ret);
+ return args.GetReturnValue().Set(ret);
+ }
+ DEBUG_HTTP2STREAM2(parent, "push stream %d created", stream->id());
+ args.GetReturnValue().Set(stream->object());
}
-void Http2Session::OnFrameError(int32_t id, uint8_t type, int error_code) {
- Local<Context> context = env()->context();
- Isolate* isolate = env()->isolate();
- HandleScope scope(isolate);
- Context::Scope context_scope(context);
- Local<Value> argv[3] = {
- Integer::New(isolate, id),
- Integer::New(isolate, type),
- Integer::New(isolate, error_code)
- };
- MakeCallback(env()->onframeerror_string(), arraysize(argv), argv);
-}
+void Http2Stream::Priority(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
-void Http2Session::OnPriority(int32_t stream,
- int32_t parent,
- int32_t weight,
- int8_t exclusive) {
- Local<Context> context = env()->context();
- Isolate* isolate = env()->isolate();
- HandleScope scope(isolate);
- Context::Scope context_scope(context);
+ Http2Priority priority(env, args[0], args[1], args[2]);
+ bool silent = args[3]->BooleanValue(context).ToChecked();
- Local<Value> argv[4] = {
- Integer::New(isolate, stream),
- Integer::New(isolate, parent),
- Integer::New(isolate, weight),
- Boolean::New(isolate, exclusive)
- };
- MakeCallback(env()->onpriority_string(), arraysize(argv), argv);
+ CHECK_EQ(stream->SubmitPriority(*priority, silent), 0);
+ DEBUG_HTTP2STREAM(stream, "priority submitted");
}
-void Http2Session::OnGoAway(int32_t lastStreamID,
- uint32_t errorCode,
- uint8_t* data,
- size_t length) {
- Local<Context> context = env()->context();
- Isolate* isolate = env()->isolate();
- HandleScope scope(isolate);
- Context::Scope context_scope(context);
- Local<Value> argv[3] = {
- Integer::NewFromUnsigned(isolate, errorCode),
- Integer::New(isolate, lastStreamID),
- Undefined(isolate)
- };
+void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Http2Stream* stream;
+ ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
- if (length > 0) {
- argv[2] = Buffer::Copy(isolate,
- reinterpret_cast<char*>(data),
- length).ToLocalChecked();
- }
+ DEBUG_HTTP2STREAM(stream, "refreshing state");
- MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
-}
+ AliasedBuffer<double, v8::Float64Array>& buffer =
+ env->http2_state()->stream_state_buffer;
-void Http2Session::OnStreamAllocImpl(size_t suggested_size,
- uv_buf_t* buf,
- void* ctx) {
- Http2Session* session = static_cast<Http2Session*>(ctx);
- buf->base = session->stream_alloc();
- buf->len = kAllocBufferSize;
+ nghttp2_stream* str = **stream;
+ nghttp2_session* s = **(stream->session());
+
+ if (str == nullptr) {
+ buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE;
+ buffer[IDX_STREAM_STATE_WEIGHT] =
+ buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
+ buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
+ buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
+ buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0;
+ } else {
+ buffer[IDX_STREAM_STATE] =
+ nghttp2_stream_get_state(str);
+ buffer[IDX_STREAM_STATE_WEIGHT] =
+ nghttp2_stream_get_weight(str);
+ buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
+ nghttp2_stream_get_sum_dependency_weight(str);
+ buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
+ nghttp2_session_get_stream_local_close(s, stream->id());
+ buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
+ nghttp2_session_get_stream_remote_close(s, stream->id());
+ buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] =
+ nghttp2_session_get_stream_local_window_size(s, stream->id());
+ }
}
+void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-void Http2Session::OnStreamReadImpl(ssize_t nread,
- const uv_buf_t* bufs,
- uv_handle_type pending,
- void* ctx) {
- Http2Session* session = static_cast<Http2Session*>(ctx);
- if (nread < 0) {
- uv_buf_t tmp_buf;
- tmp_buf.base = nullptr;
- tmp_buf.len = 0;
- session->prev_read_cb_.fn(nread,
- &tmp_buf,
- pending,
- session->prev_read_cb_.ctx);
- return;
- }
- if (nread > 0) {
- // Only pass data on if nread > 0
- uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) };
- ssize_t ret = session->Write(buf, 1);
- if (ret < 0) {
- DEBUG_HTTP2("Http2Session: fatal error receiving data: %d\n", ret);
- nghttp2_session_terminate_session(session->session(),
- NGHTTP2_PROTOCOL_ERROR);
- }
+ uint8_t* payload = nullptr;
+ if (Buffer::HasInstance(args[0])) {
+ payload = reinterpret_cast<uint8_t*>(Buffer::Data(args[0]));
+ CHECK_EQ(Buffer::Length(args[0]), 8);
}
-}
+ Http2Session::Http2Ping* ping = new Http2Ping(session);
+ Local<Object> obj = ping->object();
+ obj->Set(env->context(), env->ondone_string(), args[1]).FromJust();
-void Http2Session::Consume(Local<External> external) {
- DEBUG_HTTP2("Http2Session: consuming socket\n");
-#if defined(DEBUG) && DEBUG
- CHECK(prev_alloc_cb_.is_empty());
-#endif
- StreamBase* stream = static_cast<StreamBase*>(external->Value());
-#if defined(DEBUG) && DEBUG
- CHECK_NE(stream, nullptr);
-#endif
- stream->Consume();
- stream_ = stream;
- prev_alloc_cb_ = stream->alloc_cb();
- prev_read_cb_ = stream->read_cb();
- stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this });
- stream->set_read_cb({ Http2Session::OnStreamReadImpl, this });
-}
+ if (!session->AddPing(ping)) {
+ ping->Done(false);
+ return args.GetReturnValue().Set(false);
+ }
+ ping->Send(payload);
+ args.GetReturnValue().Set(true);
+}
-void Http2Session::Unconsume() {
- DEBUG_HTTP2("Http2Session: unconsuming socket\n");
- if (prev_alloc_cb_.is_empty())
- return;
- stream_->set_alloc_cb(prev_alloc_cb_);
- stream_->set_read_cb(prev_read_cb_);
- prev_alloc_cb_.clear();
- prev_read_cb_.clear();
- stream_ = nullptr;
+Http2Session::Http2Ping* Http2Session::PopPing() {
+ Http2Ping* ping = nullptr;
+ if (!outstanding_pings_.empty()) {
+ ping = outstanding_pings_.front();
+ outstanding_pings_.pop();
+ }
+ return ping;
}
+bool Http2Session::AddPing(Http2Session::Http2Ping* ping) {
+ if (outstanding_pings_.size() == max_outstanding_pings_)
+ return false;
+ outstanding_pings_.push(ping);
+ return true;
+}
-Headers::Headers(Isolate* isolate,
- Local<Context> context,
- Local<Array> headers) {
-#if defined(DEBUG) && DEBUG
- CHECK_EQ(headers->Length(), 2);
-#endif
- Local<Value> header_string = headers->Get(context, 0).ToLocalChecked();
- Local<Value> header_count = headers->Get(context, 1).ToLocalChecked();
-#if defined(DEBUG) && DEBUG
- CHECK(header_string->IsString());
- CHECK(header_count->IsUint32());
-#endif
- count_ = header_count.As<Uint32>()->Value();
- int header_string_len = header_string.As<String>()->Length();
+Http2Session::Http2Ping::Http2Ping(
+ Http2Session* session)
+ : AsyncWrap(session->env(),
+ session->env()->http2ping_constructor_template()
+ ->NewInstance(session->env()->context())
+ .ToLocalChecked(),
+ AsyncWrap::PROVIDER_HTTP2PING),
+ session_(session),
+ startTime_(uv_hrtime()) { }
+
+Http2Session::Http2Ping::~Http2Ping() {
+ if (!object().IsEmpty())
+ ClearWrap(object());
+ persistent().Reset();
+ CHECK(persistent().IsEmpty());
+}
- if (count_ == 0) {
- CHECK_EQ(header_string_len, 0);
- return;
+void Http2Session::Http2Ping::Send(uint8_t* payload) {
+ uint8_t data[8];
+ if (payload == nullptr) {
+ memcpy(&data, &startTime_, arraysize(data));
+ payload = data;
}
+ CHECK_EQ(nghttp2_submit_ping(**session_, NGHTTP2_FLAG_NONE, payload), 0);
+}
- // Allocate a single buffer with count_ nghttp2_nv structs, followed
- // by the raw header data as passed from JS. This looks like:
- // | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents |
- buf_.AllocateSufficientStorage((alignof(nghttp2_nv) - 1) +
- count_ * sizeof(nghttp2_nv) +
- header_string_len);
- // Make sure the start address is aligned appropriately for an nghttp2_nv*.
- char* start = reinterpret_cast<char*>(
- ROUND_UP(reinterpret_cast<uintptr_t>(*buf_), alignof(nghttp2_nv)));
- char* header_contents = start + (count_ * sizeof(nghttp2_nv));
- nghttp2_nv* const nva = reinterpret_cast<nghttp2_nv*>(start);
-
- CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length());
- CHECK_EQ(header_string.As<String>()
- ->WriteOneByte(reinterpret_cast<uint8_t*>(header_contents),
- 0, header_string_len,
- String::NO_NULL_TERMINATION),
- header_string_len);
+void Http2Session::Http2Ping::Done(bool ack, const uint8_t* payload) {
+ uint64_t end = uv_hrtime();
+ double duration = (end - startTime_) / 1e6;
- size_t n = 0;
- char* p;
- for (p = header_contents; p < header_contents + header_string_len; n++) {
- if (n >= count_) {
- // This can happen if a passed header contained a null byte. In that
- // case, just provide nghttp2 with an invalid header to make it reject
- // the headers list.
- static uint8_t zero = '\0';
- nva[0].name = nva[0].value = &zero;
- nva[0].namelen = nva[0].valuelen = 1;
- count_ = 1;
- return;
- }
-
- nva[n].flags = NGHTTP2_NV_FLAG_NONE;
- nva[n].name = reinterpret_cast<uint8_t*>(p);
- nva[n].namelen = strlen(p);
- p += nva[n].namelen + 1;
- nva[n].value = reinterpret_cast<uint8_t*>(p);
- nva[n].valuelen = strlen(p);
- p += nva[n].valuelen + 1;
+ Local<Value> buf = Undefined(env()->isolate());
+ if (payload != nullptr) {
+ buf = Buffer::Copy(env()->isolate(),
+ reinterpret_cast<const char*>(payload),
+ 8).ToLocalChecked();
}
-#if defined(DEBUG) && DEBUG
- CHECK_EQ(p, header_contents + header_string_len);
- CHECK_EQ(n, count_);
-#endif
+ Local<Value> argv[3] = {
+ Boolean::New(env()->isolate(), ack),
+ Number::New(env()->isolate(), duration),
+ buf
+ };
+ MakeCallback(env()->ondone_string(), arraysize(argv), argv);
+ delete this;
}
-
void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
@@ -1245,60 +2103,58 @@ void Initialize(Local<Object> target,
Local<String> http2SessionClassName =
FIXED_ONE_BYTE_STRING(isolate, "Http2Session");
+ Local<FunctionTemplate> ping = FunctionTemplate::New(env->isolate());
+ ping->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Ping"));
+ AsyncWrap::AddWrapMethods(env, ping);
+ Local<ObjectTemplate> pingt = ping->InstanceTemplate();
+ pingt->SetInternalFieldCount(1);
+ env->set_http2ping_constructor_template(pingt);
+
+ Local<FunctionTemplate> stream = FunctionTemplate::New(env->isolate());
+ stream->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Stream"));
+ env->SetProtoMethod(stream, "id", Http2Stream::GetID);
+ env->SetProtoMethod(stream, "destroy", Http2Stream::Destroy);
+ env->SetProtoMethod(stream, "flushData", Http2Stream::FlushData);
+ env->SetProtoMethod(stream, "priority", Http2Stream::Priority);
+ env->SetProtoMethod(stream, "pushPromise", Http2Stream::PushPromise);
+ env->SetProtoMethod(stream, "info", Http2Stream::Info);
+ env->SetProtoMethod(stream, "respondFD", Http2Stream::RespondFD);
+ env->SetProtoMethod(stream, "respond", Http2Stream::Respond);
+ env->SetProtoMethod(stream, "rstStream", Http2Stream::RstStream);
+ env->SetProtoMethod(stream, "refreshState", Http2Stream::RefreshState);
+ AsyncWrap::AddWrapMethods(env, stream);
+ StreamBase::AddMethods<Http2Stream>(env, stream, StreamBase::kFlagHasWritev);
+ Local<ObjectTemplate> streamt = stream->InstanceTemplate();
+ streamt->SetInternalFieldCount(1);
+ env->set_http2stream_constructor_template(streamt);
+ target->Set(context,
+ FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Stream"),
+ stream->GetFunction()).FromJust();
+
Local<FunctionTemplate> session =
env->NewFunctionTemplate(Http2Session::New);
session->SetClassName(http2SessionClassName);
session->InstanceTemplate()->SetInternalFieldCount(1);
AsyncWrap::AddWrapMethods(env, session);
- env->SetProtoMethod(session, "consume",
- Http2Session::Consume);
- env->SetProtoMethod(session, "destroy",
- Http2Session::Destroy);
- env->SetProtoMethod(session, "destroying",
- Http2Session::Destroying);
- env->SetProtoMethod(session, "sendHeaders",
- Http2Session::SendHeaders);
- env->SetProtoMethod(session, "submitShutdownNotice",
- Http2Session::SendShutdownNotice);
- env->SetProtoMethod(session, "submitGoaway",
- Http2Session::SubmitGoaway);
- env->SetProtoMethod(session, "submitSettings",
- Http2Session::SubmitSettings);
- env->SetProtoMethod(session, "submitPushPromise",
- Http2Session::SubmitPushPromise);
- env->SetProtoMethod(session, "submitRstStream",
- Http2Session::SubmitRstStream);
- env->SetProtoMethod(session, "submitResponse",
- Http2Session::SubmitResponse);
- env->SetProtoMethod(session, "submitFile",
- Http2Session::SubmitFile);
- env->SetProtoMethod(session, "submitRequest",
- Http2Session::SubmitRequest);
- env->SetProtoMethod(session, "submitPriority",
- Http2Session::SubmitPriority);
- env->SetProtoMethod(session, "shutdownStream",
- Http2Session::ShutdownStream);
- env->SetProtoMethod(session, "streamReadStart",
- Http2Session::StreamReadStart);
- env->SetProtoMethod(session, "streamReadStop",
- Http2Session::StreamReadStop);
+ env->SetProtoMethod(session, "ping", Http2Session::Ping);
+ env->SetProtoMethod(session, "consume", Http2Session::Consume);
+ env->SetProtoMethod(session, "destroy", Http2Session::Destroy);
+ env->SetProtoMethod(session, "destroying", Http2Session::Destroying);
+ env->SetProtoMethod(session, "shutdownNotice", Http2Session::ShutdownNotice);
+ env->SetProtoMethod(session, "goaway", Http2Session::Goaway);
+ env->SetProtoMethod(session, "settings", Http2Session::Settings);
+ env->SetProtoMethod(session, "request", Http2Session::Request);
env->SetProtoMethod(session, "setNextStreamID",
Http2Session::SetNextStreamID);
- env->SetProtoMethod(session, "destroyStream",
- Http2Session::DestroyStream);
- env->SetProtoMethod(session, "flushData",
- Http2Session::FlushData);
env->SetProtoMethod(session, "updateChunksSent",
Http2Session::UpdateChunksSent);
+ env->SetProtoMethod(session, "refreshState", Http2Session::RefreshState);
env->SetProtoMethod(
- session, "refreshLocalSettings",
+ session, "localSettings",
Http2Session::RefreshSettings<nghttp2_session_get_local_settings>);
env->SetProtoMethod(
- session, "refreshRemoteSettings",
+ session, "remoteSettings",
Http2Session::RefreshSettings<nghttp2_session_get_remote_settings>);
- StreamBase::AddMethods<Http2Session>(env, session,
- StreamBase::kFlagHasWritev |
- StreamBase::kFlagNoShutdown);
target->Set(context,
http2SessionClassName,
session->GetFunction()).FromJust();
@@ -1335,7 +2191,6 @@ void Initialize(Local<Object> target,
NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NONE);
NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NO_INDEX);
NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_DEFERRED);
- NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_NOMEM);
NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE);
NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_INVALID_ARGUMENT);
NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_CLOSED);
@@ -1386,8 +2241,6 @@ HTTP_STATUS_CODES(V)
#undef V
env->SetMethod(target, "refreshDefaultSettings", RefreshDefaultSettings);
- env->SetMethod(target, "refreshSessionState", RefreshSessionState);
- env->SetMethod(target, "refreshStreamState", RefreshStreamState);
env->SetMethod(target, "packSettings", PackSettings);
target->Set(context,
diff --git a/src/node_http2.h b/src/node_http2.h
index 8e9f8c536b..f375a249b8 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -3,7 +3,7 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
-#include "node_http2_core-inl.h"
+#include "nghttp2/nghttp2.h"
#include "node_http2_state.h"
#include "stream_base-inl.h"
#include "string_bytes.h"
@@ -19,6 +19,129 @@ using v8::EscapableHandleScope;
using v8::Isolate;
using v8::MaybeLocal;
+#ifdef NODE_DEBUG_HTTP2
+
+// Adapted from nghttp2 own debug printer
+static inline void _debug_vfprintf(const char* fmt, va_list args) {
+ vfprintf(stderr, fmt, args);
+}
+
+void inline debug_vfprintf(const char* format, ...) {
+ va_list args;
+ va_start(args, format);
+ _debug_vfprintf(format, args);
+ va_end(args);
+}
+
+#define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__);
+#define DEBUG_HTTP2SESSION(session, message) \
+ do { \
+ DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n", \
+ session->TypeName(), \
+ session->get_async_id()); \
+ } while (0)
+#define DEBUG_HTTP2SESSION2(session, message, ...) \
+ do { \
+ DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n", \
+ session->TypeName(), \
+ session->get_async_id(), \
+ __VA_ARGS__); \
+ } while (0)
+#define DEBUG_HTTP2STREAM(stream, message) \
+ do { \
+ DEBUG_HTTP2("Http2Stream %d (%.0lf) [Http2Session %s (%.0lf)] " message \
+ "\n", stream->id(), stream->get_async_id(), \
+ stream->session()->TypeName(), \
+ stream->session()->get_async_id()); \
+ } while (0)
+#define DEBUG_HTTP2STREAM2(stream, message, ...) \
+ do { \
+ DEBUG_HTTP2("Http2Stream %d (%.0lf) [Http2Session %s (%.0lf)] " message \
+ "\n", stream->id(), stream->get_async_id(), \
+ stream->session()->TypeName(), \
+ stream->session()->get_async_id(), \
+ __VA_ARGS__); \
+ } while (0)
+#else
+#define DEBUG_HTTP2(...) do {} while (0)
+#define DEBUG_HTTP2SESSION(...) do {} while (0)
+#define DEBUG_HTTP2SESSION2(...) do {} while (0)
+#define DEBUG_HTTP2STREAM(...) do {} while (0)
+#define DEBUG_HTTP2STREAM2(...) do {} while (0)
+#endif
+
+#define DEFAULT_MAX_PINGS 10
+#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
+#define DEFAULT_SETTINGS_ENABLE_PUSH 1
+#define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535
+#define DEFAULT_SETTINGS_MAX_FRAME_SIZE 16384
+#define DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE 65535
+#define MAX_MAX_FRAME_SIZE 16777215
+#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE
+#define MAX_INITIAL_WINDOW_SIZE 2147483647
+
+#define MAX_MAX_HEADER_LIST_SIZE 16777215u
+#define DEFAULT_MAX_HEADER_LIST_PAIRS 128u
+
+struct nghttp2_stream_write_t;
+
+#define MAX_BUFFER_COUNT 16
+
+enum nghttp2_session_type {
+ NGHTTP2_SESSION_SERVER,
+ NGHTTP2_SESSION_CLIENT
+};
+
+enum nghttp2_shutdown_flags {
+ NGHTTP2_SHUTDOWN_FLAG_GRACEFUL
+};
+
+enum nghttp2_stream_flags {
+ NGHTTP2_STREAM_FLAG_NONE = 0x0,
+ // Writable side has ended
+ NGHTTP2_STREAM_FLAG_SHUT = 0x1,
+ // Reading has started
+ NGHTTP2_STREAM_FLAG_READ_START = 0x2,
+ // Reading is paused
+ NGHTTP2_STREAM_FLAG_READ_PAUSED = 0x4,
+ // Stream is closed
+ NGHTTP2_STREAM_FLAG_CLOSED = 0x8,
+ // Stream is destroyed
+ NGHTTP2_STREAM_FLAG_DESTROYED = 0x10,
+ // Stream has trailers
+ NGHTTP2_STREAM_FLAG_TRAILERS = 0x20
+};
+
+enum nghttp2_stream_options {
+ STREAM_OPTION_EMPTY_PAYLOAD = 0x1,
+ STREAM_OPTION_GET_TRAILERS = 0x2,
+};
+
+// Callbacks
+typedef void (*nghttp2_stream_write_cb)(
+ nghttp2_stream_write_t* req,
+ int status);
+
+struct nghttp2_stream_write {
+ unsigned int nbufs = 0;
+ nghttp2_stream_write_t* req = nullptr;
+ nghttp2_stream_write_cb cb = nullptr;
+ MaybeStackBuffer<uv_buf_t, MAX_BUFFER_COUNT> bufs;
+};
+
+struct nghttp2_header {
+ nghttp2_rcbuf* name = nullptr;
+ nghttp2_rcbuf* value = nullptr;
+ uint8_t flags = 0;
+};
+
+
+
+struct nghttp2_stream_write_t {
+ void* data;
+ int status;
+};
+
// Unlike the HTTP/1 implementation, the HTTP/2 implementation is not limited
// to a fixed number of known supported HTTP methods. These constants, therefore
// are provided strictly as a convenience to users and are exposed via the
@@ -292,6 +415,11 @@ const char* nghttp2_errname(int rv) {
}
}
+enum session_state_flags {
+ SESSION_STATE_NONE = 0x0,
+ SESSION_STATE_DESTROYING = 0x1
+};
+
// This allows for 4 default-sized frames with their frame headers
static const size_t kAllocBufferSize = 4 * (16384 + 9);
@@ -299,6 +427,7 @@ typedef uint32_t(*get_setting)(nghttp2_session* session,
nghttp2_settings_id id);
class Http2Session;
+class Http2Stream;
// The Http2Options class is used to parse the options object passed in to
// a Http2Session object and convert those into an appropriate nghttp2_option
@@ -332,10 +461,19 @@ class Http2Options {
return padding_strategy_;
}
+ void SetMaxOutstandingPings(size_t max) {
+ max_outstanding_pings_ = max;
+ }
+
+ size_t GetMaxOutstandingPings() {
+ return max_outstanding_pings_;
+ }
+
private:
nghttp2_option* options_;
uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
+ size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
};
// The Http2Settings class is used to parse the settings passed in for
@@ -382,82 +520,133 @@ class Http2Priority {
nghttp2_priority_spec spec;
};
-class Http2Session : public AsyncWrap,
- public StreamBase,
- public Nghttp2Session {
+class Http2Stream : public AsyncWrap,
+ public StreamBase {
public:
- Http2Session(Environment* env,
- Local<Object> wrap,
- nghttp2_session_type type);
- ~Http2Session() override;
+ Http2Stream(Http2Session* session,
+ int32_t id,
+ nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS,
+ int options = 0);
+ ~Http2Stream() override;
- static void OnStreamAllocImpl(size_t suggested_size,
- uv_buf_t* buf,
- void* ctx);
- static void OnStreamReadImpl(ssize_t nread,
- const uv_buf_t* bufs,
- uv_handle_type pending,
- void* ctx);
+ nghttp2_stream* operator*();
- protected:
- ssize_t OnMaxFrameSizePadding(size_t frameLength,
- size_t maxPayloadLen);
+ Http2Session* session() { return session_; }
+
+ // Queue outbound chunks of data to be sent on this stream
+ inline int Write(
+ nghttp2_stream_write_t* req,
+ const uv_buf_t bufs[],
+ unsigned int nbufs,
+ nghttp2_stream_write_cb cb);
+
+ inline void AddChunk(const uint8_t* data, size_t len);
+
+ inline void FlushDataChunks();
+
+ // Process a Data Chunk
+ void OnDataChunk(uv_buf_t* chunk);
+
+
+ // Required for StreamBase
+ int ReadStart() override;
+
+ // Required for StreamBase
+ int ReadStop() override;
+
+ // Required for StreamBase
+ int DoShutdown(ShutdownWrap* req_wrap);
+
+ // Initiate a response on this stream.
+ inline int SubmitResponse(nghttp2_nv* nva,
+ size_t len,
+ int options);
+
+ // Send data read from a file descriptor as the response on this stream.
+ inline int SubmitFile(int fd,
+ nghttp2_nv* nva, size_t len,
+ int64_t offset,
+ int64_t length,
+ int options);
+
+ // Submit informational headers for this stream
+ inline int SubmitInfo(nghttp2_nv* nva, size_t len);
+
+ // Submit a PRIORITY frame for this stream
+ inline int SubmitPriority(nghttp2_priority_spec* prispec,
+ bool silent = false);
+
+ // Submits an RST_STREAM frame using the given code
+ inline int SubmitRstStream(const uint32_t code);
+
+ // Submits a PUSH_PROMISE frame with this stream as the parent.
+ inline Http2Stream* SubmitPushPromise(
+ nghttp2_nv* nva,
+ size_t len,
+ int32_t* ret,
+ int options = 0);
+
+
+ inline void Close(int32_t code);
- ssize_t OnCallbackPadding(size_t frame,
- size_t maxPayloadLen);
+ // Shutdown the writable side of the stream
+ inline void Shutdown();
- bool HasGetPaddingCallback() override {
- return padding_strategy_ == PADDING_STRATEGY_MAX ||
- padding_strategy_ == PADDING_STRATEGY_CALLBACK;
+ // Destroy this stream instance and free all held memory.
+ inline void Destroy();
+
+ inline bool IsDestroyed() const {
+ return flags_ & NGHTTP2_STREAM_FLAG_DESTROYED;
+ }
+
+ inline bool IsWritable() const {
+ return !(flags_ & NGHTTP2_STREAM_FLAG_SHUT);
}
- ssize_t GetPadding(size_t frameLength, size_t maxPayloadLen) override {
- if (padding_strategy_ == PADDING_STRATEGY_MAX) {
- return OnMaxFrameSizePadding(frameLength, maxPayloadLen);
+ inline bool IsPaused() const {
+ return flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED;
+ }
+
+ inline bool IsClosed() const {
+ return flags_ & NGHTTP2_STREAM_FLAG_CLOSED;
}
-#if defined(DEBUG) && DEBUG
- CHECK_EQ(padding_strategy_, PADDING_STRATEGY_CALLBACK);
-#endif
+ inline bool HasTrailers() const {
+ return flags_ & NGHTTP2_STREAM_FLAG_TRAILERS;
+ }
- return OnCallbackPadding(frameLength, maxPayloadLen);
- }
-
- void OnHeaders(
- Nghttp2Stream* stream,
- nghttp2_header* headers,
- size_t count,
- nghttp2_headers_category cat,
- uint8_t flags) override;
- void OnStreamClose(int32_t id, uint32_t code) override;
- void OnDataChunk(Nghttp2Stream* stream, uv_buf_t* chunk) override;
- void OnSettings(bool ack) override;
- void OnPriority(int32_t stream,
- int32_t parent,
- int32_t weight,
- int8_t exclusive) override;
- void OnGoAway(int32_t lastStreamID,
- uint32_t errorCode,
- uint8_t* data,
- size_t length) override;
- void OnFrameError(int32_t id, uint8_t type, int error_code) override;
- void OnTrailers(Nghttp2Stream* stream,
- const SubmitTrailers& submit_trailers) override;
-
- void Send(WriteWrap* req, char* buf, size_t length) override;
- WriteWrap* AllocateSend() override;
+ // Returns true if this stream is in the reading state, which occurs when
+ // the NGHTTP2_STREAM_FLAG_READ_START flag has been set and the
+ // NGHTTP2_STREAM_FLAG_READ_PAUSED flag is *not* set.
+ inline bool IsReading() const {
+ return flags_ & NGHTTP2_STREAM_FLAG_READ_START &&
+ !(flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED);
+ }
- int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count,
- uv_stream_t* send_handle) override;
+ // Returns the RST_STREAM code used to close this stream
+ inline int32_t code() const { return code_; }
+
+ // Returns the stream identifier for this stream
+ inline int32_t id() const { return id_; }
- AsyncWrap* GetAsyncWrap() override {
- return static_cast<AsyncWrap*>(this);
+ inline bool AddHeader(nghttp2_rcbuf* name,
+ nghttp2_rcbuf* value,
+ uint8_t flags);
+
+ inline nghttp2_header* headers() {
+ return current_headers_.data();
+ }
+
+ inline nghttp2_headers_category headers_category() const {
+ return current_headers_category_;
}
- void* Cast() override {
- return reinterpret_cast<void*>(this);
+ inline size_t headers_count() const {
+ return current_headers_.size();
}
+ void StartHeaders(nghttp2_headers_category category);
+
// Required for StreamBase
bool IsAlive() override {
return true;
@@ -468,47 +657,215 @@ class Http2Session : public AsyncWrap,
return false;
}
- // Required for StreamBase
- int ReadStart() override { return 0; }
+ AsyncWrap* GetAsyncWrap() override { return static_cast<AsyncWrap*>(this); }
+ void* Cast() override { return reinterpret_cast<void*>(this); }
- // Required for StreamBase
- int ReadStop() override { return 0; }
+ int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count,
+ uv_stream_t* send_handle) override;
- // Required for StreamBase
- int DoShutdown(ShutdownWrap* req_wrap) override {
- return 0;
- }
+ size_t self_size() const override { return sizeof(*this); }
- uv_loop_t* event_loop() const override {
- return env()->event_loop();
+ // Handling Trailer Headers
+ class SubmitTrailers {
+ public:
+ inline void Submit(nghttp2_nv* trailers, size_t length) const;
+
+ inline SubmitTrailers(Http2Session* sesion,
+ Http2Stream* stream,
+ uint32_t* flags);
+
+ private:
+ Http2Session* const session_;
+ Http2Stream* const stream_;
+ uint32_t* const flags_;
+
+ friend class Http2Stream;
+ };
+
+ void OnTrailers(const SubmitTrailers& submit_trailers);
+
+ // JavaScript API
+ static void GetID(const FunctionCallbackInfo<Value>& args);
+ static void Destroy(const FunctionCallbackInfo<Value>& args);
+ static void FlushData(const FunctionCallbackInfo<Value>& args);
+ static void Priority(const FunctionCallbackInfo<Value>& args);
+ static void PushPromise(const FunctionCallbackInfo<Value>& args);
+ static void RefreshState(const FunctionCallbackInfo<Value>& args);
+ static void Info(const FunctionCallbackInfo<Value>& args);
+ static void RespondFD(const FunctionCallbackInfo<Value>& args);
+ static void Respond(const FunctionCallbackInfo<Value>& args);
+ static void RstStream(const FunctionCallbackInfo<Value>& args);
+
+ class Provider;
+
+ private:
+ Http2Session* session_; // The Parent HTTP/2 Session
+ int32_t id_; // The Stream Identifier
+ int32_t code_ = NGHTTP2_NO_ERROR; // The RST_STREAM code (if any)
+ int flags_ = NGHTTP2_STREAM_FLAG_NONE; // Internal state flags
+
+ uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
+ uint32_t max_header_length_ = DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE;
+
+ // The Current Headers block... As headers are received for this stream,
+ // they are temporarily stored here until the OnFrameReceived is called
+ // signalling the end of the HEADERS frame
+ nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS;
+ uint32_t current_headers_length_ = 0; // total number of octets
+ std::vector<nghttp2_header> current_headers_;
+
+ // Inbound Data... This is the data received via DATA frames for this stream.
+ std::queue<uv_buf_t> data_chunks_;
+
+ // Outbound Data... This is the data written by the JS layer that is
+ // waiting to be written out to the socket.
+ std::queue<nghttp2_stream_write*> queue_;
+ unsigned int queue_index_ = 0;
+ size_t queue_offset_ = 0;
+ int64_t fd_offset_ = 0;
+ int64_t fd_length_ = -1;
+};
+
+class Http2Stream::Provider {
+ public:
+ Provider(Http2Stream* stream, int options);
+ explicit Provider(int options);
+ virtual ~Provider();
+
+ nghttp2_data_provider* operator*() {
+ return !empty_ ? &provider_ : nullptr;
}
+
+ class FD;
+ class Stream;
+ protected:
+ nghttp2_data_provider provider_;
+
+ private:
+ bool empty_ = false;
+};
+
+class Http2Stream::Provider::FD : public Http2Stream::Provider {
+ public:
+ FD(int options, int fd);
+ FD(Http2Stream* stream, int options, int fd);
+
+ static ssize_t OnRead(nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data);
+};
+
+class Http2Stream::Provider::Stream : public Http2Stream::Provider {
public:
+ Stream(Http2Stream* stream, int options);
+ explicit Stream(int options);
+
+ static ssize_t OnRead(nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data);
+};
+
+
+class Http2Session : public AsyncWrap {
+ public:
+ Http2Session(Environment* env,
+ Local<Object> wrap,
+ nghttp2_session_type type = NGHTTP2_SESSION_SERVER);
+ ~Http2Session() override;
+
+ class Http2Ping;
+
+ void Start();
+ void Stop();
+ void Close();
void Consume(Local<External> external);
void Unconsume();
+ bool Ping(v8::Local<v8::Function> function);
+
+ inline void SendPendingData();
+
+ // Submits a new request. If the request is a success, assigned
+ // will be a pointer to the Http2Stream instance assigned.
+ // This only works if the session is a client session.
+ inline Http2Stream* SubmitRequest(
+ nghttp2_priority_spec* prispec,
+ nghttp2_nv* nva,
+ size_t len,
+ int32_t* ret,
+ int options = 0);
+
+ nghttp2_session_type type() const { return session_type_; }
+
+ inline nghttp2_session* session() const { return session_; }
+
+ nghttp2_session* operator*() { return session_; }
+
+ uint32_t GetMaxHeaderPairs() const { return max_header_pairs_; }
+
+ inline const char* TypeName();
+
+ inline void MarkDestroying() { flags_ |= SESSION_STATE_DESTROYING; }
+ inline bool IsDestroying() { return flags_ & SESSION_STATE_DESTROYING; }
+
+ // Returns pointer to the stream, or nullptr if stream does not exist
+ inline Http2Stream* FindStream(int32_t id);
+
+ // Adds a stream instance to this session
+ inline void AddStream(Http2Stream* stream);
+
+ // Removes a stream instance from this session
+ inline void RemoveStream(int32_t id);
+
+ // Sends a notice to the connected peer that the session is shutting down.
+ inline void SubmitShutdownNotice();
+
+ // Submits a SETTINGS frame to the connected peer.
+ inline void Settings(const nghttp2_settings_entry iv[], size_t niv);
+
+ // Write data to the session
+ inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
+
+ inline void SetChunksSinceLastWrite(size_t n = 0);
+
+ size_t self_size() const override { return sizeof(*this); }
+
+ char* stream_alloc() {
+ return stream_buf_;
+ }
+
+ inline void GetTrailers(Http2Stream* stream, uint32_t* flags);
+
+ static void OnStreamAllocImpl(size_t suggested_size,
+ uv_buf_t* buf,
+ void* ctx);
+ static void OnStreamReadImpl(ssize_t nread,
+ const uv_buf_t* bufs,
+ uv_handle_type pending,
+ void* ctx);
+
+ // The JavaScript API
static void New(const FunctionCallbackInfo<Value>& args);
static void Consume(const FunctionCallbackInfo<Value>& args);
static void Unconsume(const FunctionCallbackInfo<Value>& args);
static void Destroying(const FunctionCallbackInfo<Value>& args);
static void Destroy(const FunctionCallbackInfo<Value>& args);
- static void SubmitSettings(const FunctionCallbackInfo<Value>& args);
- static void SubmitRstStream(const FunctionCallbackInfo<Value>& args);
- static void SubmitResponse(const FunctionCallbackInfo<Value>& args);
- static void SubmitFile(const FunctionCallbackInfo<Value>& args);
- static void SubmitRequest(const FunctionCallbackInfo<Value>& args);
- static void SubmitPushPromise(const FunctionCallbackInfo<Value>& args);
- static void SubmitPriority(const FunctionCallbackInfo<Value>& args);
- static void SendHeaders(const FunctionCallbackInfo<Value>& args);
- static void ShutdownStream(const FunctionCallbackInfo<Value>& args);
- static void StreamWrite(const FunctionCallbackInfo<Value>& args);
- static void StreamReadStart(const FunctionCallbackInfo<Value>& args);
- static void StreamReadStop(const FunctionCallbackInfo<Value>& args);
+ static void Settings(const FunctionCallbackInfo<Value>& args);
+ static void Request(const FunctionCallbackInfo<Value>& args);
static void SetNextStreamID(const FunctionCallbackInfo<Value>& args);
- static void SendShutdownNotice(const FunctionCallbackInfo<Value>& args);
- static void SubmitGoaway(const FunctionCallbackInfo<Value>& args);
- static void DestroyStream(const FunctionCallbackInfo<Value>& args);
- static void FlushData(const FunctionCallbackInfo<Value>& args);
+ static void ShutdownNotice(const FunctionCallbackInfo<Value>& args);
+ static void Goaway(const FunctionCallbackInfo<Value>& args);
static void UpdateChunksSent(const FunctionCallbackInfo<Value>& args);
+ static void RefreshState(const FunctionCallbackInfo<Value>& args);
+ static void Ping(const FunctionCallbackInfo<Value>& args);
template <get_setting fn>
static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
@@ -516,17 +873,125 @@ class Http2Session : public AsyncWrap,
template <get_setting fn>
static void GetSettings(const FunctionCallbackInfo<Value>& args);
- size_t self_size() const override {
- return sizeof(*this);
- }
+ void Send(WriteWrap* req, char* buf, size_t length);
+ WriteWrap* AllocateSend();
- char* stream_alloc() {
- return stream_buf_;
+ uv_loop_t* event_loop() const {
+ return env()->event_loop();
}
- void Close() override;
+ Http2Ping* PopPing();
+ bool AddPing(Http2Ping* ping);
private:
+ // Frame Padding Strategies
+ inline ssize_t OnMaxFrameSizePadding(size_t frameLength,
+ size_t maxPayloadLen);
+ inline ssize_t OnCallbackPadding(size_t frame,
+ size_t maxPayloadLen);
+
+ // Frame Handler
+ inline void HandleDataFrame(const nghttp2_frame* frame);
+ inline void HandleGoawayFrame(const nghttp2_frame* frame);
+ inline void HandleHeadersFrame(const nghttp2_frame* frame);
+ inline void HandlePriorityFrame(const nghttp2_frame* frame);
+ inline void HandleSettingsFrame(const nghttp2_frame* frame);
+ inline void HandlePingFrame(const nghttp2_frame* frame);
+
+ // nghttp2 callbacks
+ static inline int OnBeginHeadersCallback(
+ nghttp2_session* session,
+ const nghttp2_frame* frame,
+ void* user_data);
+ static inline int OnHeaderCallback(
+ nghttp2_session* session,
+ const nghttp2_frame* frame,
+ nghttp2_rcbuf* name,
+ nghttp2_rcbuf* value,
+ uint8_t flags,
+ void* user_data);
+ static inline int OnFrameReceive(
+ nghttp2_session* session,
+ const nghttp2_frame* frame,
+ void* user_data);
+ static inline int OnFrameNotSent(
+ nghttp2_session* session,
+ const nghttp2_frame* frame,
+ int error_code,
+ void* user_data);
+ static inline int OnStreamClose(
+ nghttp2_session* session,
+ int32_t id,
+ uint32_t code,
+ void* user_data);
+ static inline int OnInvalidHeader(
+ nghttp2_session* session,
+ const nghttp2_frame* frame,
+ nghttp2_rcbuf* name,
+ nghttp2_rcbuf* value,
+ uint8_t flags,
+ void* user_data);
+ static inline int OnDataChunkReceived(
+ nghttp2_session* session,
+ uint8_t flags,
+ int32_t id,
+ const uint8_t* data,
+ size_t len,
+ void* user_data);
+ static inline ssize_t OnSelectPadding(
+ nghttp2_session* session,
+ const nghttp2_frame* frame,
+ size_t maxPayloadLen,
+ void* user_data);
+ static inline int OnNghttpError(
+ nghttp2_session* session,
+ const char* message,
+ size_t len,
+ void* user_data);
+
+
+ static inline ssize_t OnStreamReadFD(
+ nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data);
+ static inline ssize_t OnStreamRead(
+ nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data);
+
+ struct Callbacks {
+ inline explicit Callbacks(bool kHasGetPaddingCallback);
+ inline ~Callbacks();
+
+ nghttp2_session_callbacks* callbacks;
+ };
+
+ /* Use callback_struct_saved[kHasGetPaddingCallback ? 1 : 0] */
+ static const Callbacks callback_struct_saved[2];
+
+ // The underlying nghttp2_session handle
+ nghttp2_session* session_;
+
+ // The session type: client or server
+ nghttp2_session_type session_type_;
+
+ // The maximum number of header pairs permitted for streams on this session
+ uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
+
+ // The collection of active Http2Streams associated with this session
+ std::unordered_map<int32_t, Http2Stream*> streams_;
+
+ int flags_ = SESSION_STATE_NONE;
+
+ // The StreamBase instance being used for i/o
StreamBase* stream_;
StreamResource::Callback<StreamResource::AllocCb> prev_alloc_cb_;
StreamResource::Callback<StreamResource::ReadCb> prev_read_cb_;
@@ -534,9 +999,27 @@ class Http2Session : public AsyncWrap,
// use this to allow timeout tracking during long-lasting writes
uint32_t chunks_sent_since_last_write_ = 0;
- uv_prepare_t* prep_ = nullptr;
+ uv_prepare_t* prep_ = nullptr;
char stream_buf_[kAllocBufferSize];
+
+ size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
+ std::queue<Http2Ping*> outstanding_pings_;
+};
+
+class Http2Session::Http2Ping : public AsyncWrap {
+ public:
+ explicit Http2Ping(Http2Session* session);
+ ~Http2Ping();
+
+ size_t self_size() const override { return sizeof(*this); }
+
+ void Send(uint8_t* payload);
+ void Done(bool ack, const uint8_t* payload = nullptr);
+
+ private:
+ Http2Session* session_;
+ uint64_t startTime_;
};
class ExternalHeader :
diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h
deleted file mode 100644
index de38a5df47..0000000000
--- a/src/node_http2_core-inl.h
+++ /dev/null
@@ -1,925 +0,0 @@
-#ifndef SRC_NODE_HTTP2_CORE_INL_H_
-#define SRC_NODE_HTTP2_CORE_INL_H_
-
-#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
-
-#include "node_http2_core.h"
-#include "node_internals.h" // arraysize
-#include <algorithm>
-
-namespace node {
-namespace http2 {
-
-#ifdef NODE_DEBUG_HTTP2
-inline int Nghttp2Session::OnNghttpError(nghttp2_session* session,
- const char* message,
- size_t len,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- DEBUG_HTTP2("Nghttp2Session %s: Error '%.*s'\n",
- handle->TypeName(), len, message);
- return 0;
-}
-#endif
-
-inline int32_t GetFrameID(const nghttp2_frame* frame) {
- // If this is a push promise, we want to grab the id of the promised stream
- return (frame->hd.type == NGHTTP2_PUSH_PROMISE) ?
- frame->push_promise.promised_stream_id :
- frame->hd.stream_id;
-}
-
-// nghttp2 calls this at the beginning a new HEADERS or PUSH_PROMISE frame.
-// We use it to ensure that an Nghttp2Stream instance is allocated to store
-// the state.
-inline int Nghttp2Session::OnBeginHeadersCallback(nghttp2_session* session,
- const nghttp2_frame* frame,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- int32_t id = GetFrameID(frame);
- DEBUG_HTTP2("Nghttp2Session %s: beginning headers for stream %d\n",
- handle->TypeName(), id);
-
- Nghttp2Stream* stream = handle->FindStream(id);
- if (stream == nullptr) {
- new Nghttp2Stream(id, handle, frame->headers.cat);
- } else {
- stream->StartHeaders(frame->headers.cat);
- }
- return 0;
-}
-
-inline size_t GetBufferLength(nghttp2_rcbuf* buf) {
- return nghttp2_rcbuf_get_buf(buf).len;
-}
-
-inline bool Nghttp2Stream::AddHeader(nghttp2_rcbuf* name,
- nghttp2_rcbuf* value,
- uint8_t flags) {
- size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
- if (current_headers_.size() == max_header_pairs_ ||
- current_headers_length_ + length > max_header_length_) {
- return false;
- }
- nghttp2_header header;
- header.name = name;
- header.value = value;
- header.flags = flags;
- current_headers_.push_back(header);
- nghttp2_rcbuf_incref(name);
- nghttp2_rcbuf_incref(value);
- current_headers_length_ += length;
- return true;
-}
-
-// nghttp2 calls this once for every header name-value pair in a HEADERS
-// or PUSH_PROMISE block. CONTINUATION frames are handled automatically
-// and transparently so we do not need to worry about those at all.
-inline int Nghttp2Session::OnHeaderCallback(nghttp2_session* session,
- const nghttp2_frame* frame,
- nghttp2_rcbuf* name,
- nghttp2_rcbuf* value,
- uint8_t flags,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- int32_t id = GetFrameID(frame);
- Nghttp2Stream* stream = handle->FindStream(id);
- if (!stream->AddHeader(name, value, flags)) {
- // This will only happen if the connected peer sends us more
- // than the allowed number of header items at any given time
- stream->SubmitRstStream(NGHTTP2_ENHANCE_YOUR_CALM);
- return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
- }
- return 0;
-}
-
-
-// When nghttp2 has completely processed a frame, it calls OnFrameReceive.
-// It is our responsibility to delegate out from there. We can ignore most
-// control frames since nghttp2 will handle those for us.
-inline int Nghttp2Session::OnFrameReceive(nghttp2_session* session,
- const nghttp2_frame* frame,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- DEBUG_HTTP2("Nghttp2Session %s: complete frame received: type: %d\n",
- handle->TypeName(), frame->hd.type);
- bool ack;
- switch (frame->hd.type) {
- case NGHTTP2_DATA:
- handle->HandleDataFrame(frame);
- break;
- case NGHTTP2_PUSH_PROMISE:
- // Intentional fall-through, handled just like headers frames
- case NGHTTP2_HEADERS:
- handle->HandleHeadersFrame(frame);
- break;
- case NGHTTP2_SETTINGS:
- ack = (frame->hd.flags & NGHTTP2_FLAG_ACK) == NGHTTP2_FLAG_ACK;
- handle->OnSettings(ack);
- break;
- case NGHTTP2_PRIORITY:
- handle->HandlePriorityFrame(frame);
- break;
- case NGHTTP2_GOAWAY:
- handle->HandleGoawayFrame(frame);
- break;
- default:
- break;
- }
- return 0;
-}
-
-// nghttp2 will call this if an error occurs attempting to send a frame.
-// Unless the stream or session is closed, this really should not happen
-// unless there is a serious flaw in our implementation.
-inline int Nghttp2Session::OnFrameNotSent(nghttp2_session* session,
- const nghttp2_frame* frame,
- int error_code,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- DEBUG_HTTP2("Nghttp2Session %s: frame type %d was not sent, code: %d\n",
- handle->TypeName(), frame->hd.type, error_code);
- // Do not report if the frame was not sent due to the session closing
- if (error_code != NGHTTP2_ERR_SESSION_CLOSING &&
- error_code != NGHTTP2_ERR_STREAM_CLOSED &&
- error_code != NGHTTP2_ERR_STREAM_CLOSING) {
- handle->OnFrameError(frame->hd.stream_id,
- frame->hd.type,
- error_code);
- }
- return 0;
-}
-
-inline int Nghttp2Session::OnInvalidHeader(nghttp2_session* session,
- const nghttp2_frame* frame,
- nghttp2_rcbuf* name,
- nghttp2_rcbuf* value,
- uint8_t flags,
- void* user_data) {
- // Ignore invalid header fields by default.
- return 0;
-}
-
-// Called when nghttp2 closes a stream, either in response to an RST_STREAM
-// frame or the stream closing naturally on it's own
-inline int Nghttp2Session::OnStreamClose(nghttp2_session* session,
- int32_t id,
- uint32_t code,
- void* user_data) {
- Nghttp2Session*handle = static_cast<Nghttp2Session*>(user_data);
- DEBUG_HTTP2("Nghttp2Session %s: stream %d closed, code: %d\n",
- handle->TypeName(), id, code);
- Nghttp2Stream* stream = handle->FindStream(id);
- // Intentionally ignore the callback if the stream does not exist
- if (stream != nullptr)
- stream->Close(code);
- return 0;
-}
-
-// Called by nghttp2 to collect the data while a file response is sent.
-// The buf is the DATA frame buffer that needs to be filled with at most
-// length bytes. flags is used to control what nghttp2 does next.
-inline ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session,
- int32_t id,
- uint8_t* buf,
- size_t length,
- uint32_t* flags,
- nghttp2_data_source* source,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- DEBUG_HTTP2("Nghttp2Session %s: reading outbound file data for stream %d\n",
- handle->TypeName(), id);
- Nghttp2Stream* stream = handle->FindStream(id);
-
- int fd = source->fd;
- int64_t offset = stream->fd_offset_;
- ssize_t numchars = 0;
-
- if (stream->fd_length_ >= 0 &&
- stream->fd_length_ < static_cast<int64_t>(length))
- length = stream->fd_length_;
-
- uv_buf_t data;
- data.base = reinterpret_cast<char*>(buf);
- data.len = length;
-
- uv_fs_t read_req;
-
- if (length > 0) {
- // TODO(addaleax): Never use synchronous I/O on the main thread.
- numchars = uv_fs_read(handle->event_loop(),
- &read_req,
- fd, &data, 1,
- offset, nullptr);
- uv_fs_req_cleanup(&read_req);
- }
-
- // Close the stream with an error if reading fails
- if (numchars < 0)
- return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
-
- // Update the read offset for the next read
- stream->fd_offset_ += numchars;
- stream->fd_length_ -= numchars;
-
- // if numchars < length, assume that we are done.
- if (static_cast<size_t>(numchars) < length || length <= 0) {
- DEBUG_HTTP2("Nghttp2Session %s: no more data for stream %d\n",
- handle->TypeName(), id);
- *flags |= NGHTTP2_DATA_FLAG_EOF;
- GetTrailers(session, handle, stream, flags);
- }
-
- return numchars;
-}
-
-// Called by nghttp2 to collect the data to pack within a DATA frame.
-// The buf is the DATA frame buffer that needs to be filled with at most
-// length bytes. flags is used to control what nghttp2 does next.
-inline ssize_t Nghttp2Session::OnStreamRead(nghttp2_session* session,
- int32_t id,
- uint8_t* buf,
- size_t length,
- uint32_t* flags,
- nghttp2_data_source* source,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- DEBUG_HTTP2("Nghttp2Session %s: reading outbound data for stream %d\n",
- handle->TypeName(), id);
- Nghttp2Stream* stream = handle->FindStream(id);
- size_t remaining = length;
- size_t offset = 0;
-
- // While there is data in the queue, copy data into buf until it is full.
- // There may be data left over, which will be sent the next time nghttp
- // calls this callback.
- while (!stream->queue_.empty()) {
- DEBUG_HTTP2("Nghttp2Session %s: processing outbound data chunk\n",
- handle->TypeName());
- nghttp2_stream_write* head = stream->queue_.front();
- while (stream->queue_index_ < head->nbufs) {
- if (remaining == 0)
- goto end;
-
- unsigned int n = stream->queue_index_;
- // len is the number of bytes in head->bufs[n] that are yet to be written
- size_t len = head->bufs[n].len - stream->queue_offset_;
- size_t bytes_to_write = len < remaining ? len : remaining;
- memcpy(buf + offset,
- head->bufs[n].base + stream->queue_offset_,
- bytes_to_write);
- offset += bytes_to_write;
- remaining -= bytes_to_write;
- if (bytes_to_write < len) {
- stream->queue_offset_ += bytes_to_write;
- } else {
- stream->queue_index_++;
- stream->queue_offset_ = 0;
- }
- }
- stream->queue_offset_ = 0;
- stream->queue_index_ = 0;
- head->cb(head->req, 0);
- delete head;
- stream->queue_.pop();
- }
-
-end:
- // If we are no longer writable and there is no more data in the queue,
- // then we need to set the NGHTTP2_DATA_FLAG_EOF flag.
- // If we are still writable but there is not yet any data to send, set the
- // NGHTTP2_ERR_DEFERRED flag. This will put the stream into a pending state
- // that will wait for data to become available.
- // If neither of these flags are set, then nghttp2 will call this callback
- // again to get the data for the next DATA frame.
- int writable = !stream->queue_.empty() || stream->IsWritable();
- if (offset == 0 && writable && stream->queue_.empty()) {
- DEBUG_HTTP2("Nghttp2Session %s: deferring stream %d\n",
- handle->TypeName(), id);
- return NGHTTP2_ERR_DEFERRED;
- }
- if (!writable) {
- DEBUG_HTTP2("Nghttp2Session %s: no more data for stream %d\n",
- handle->TypeName(), id);
- *flags |= NGHTTP2_DATA_FLAG_EOF;
-
- GetTrailers(session, handle, stream, flags);
- }
-#if defined(DEBUG) && DEBUG
- CHECK(offset <= length);
-#endif
- return offset;
-}
-
-// Called by nghttp2 when it needs to determine how much padding to apply
-// to a DATA or HEADERS frame
-inline ssize_t Nghttp2Session::OnSelectPadding(nghttp2_session* session,
- const nghttp2_frame* frame,
- size_t maxPayloadLen,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
-#if defined(DEBUG) && DEBUG
- CHECK(handle->HasGetPaddingCallback());
-#endif
- ssize_t padding = handle->GetPadding(frame->hd.length, maxPayloadLen);
- DEBUG_HTTP2("Nghttp2Session %s: using padding, size: %d\n",
- handle->TypeName(), padding);
- return padding;
-}
-
-// While nghttp2 is processing a DATA frame, it will call the
-// OnDataChunkReceived callback multiple times, passing along individual
-// chunks of data from the DATA frame payload. These *must* be memcpy'd
-// out because the pointer to the data will quickly become invalid.
-inline int Nghttp2Session::OnDataChunkReceived(nghttp2_session* session,
- uint8_t flags,
- int32_t id,
- const uint8_t* data,
- size_t len,
- void* user_data) {
- Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
- DEBUG_HTTP2("Nghttp2Session %s: buffering data chunk for stream %d, size: "
- "%d, flags: %d\n", handle->TypeName(),
- id, len, flags);
- // We should never actually get a 0-length chunk so this check is
- // only a precaution at this point.
- if (len > 0) {
- nghttp2_session_consume_connection(session, len);
- Nghttp2Stream* stream = handle->FindStream(id);
- char* buf = Malloc<char>(len);
- memcpy(buf, data, len);
- stream->data_chunks_.emplace(uv_buf_init(buf, len));
- }
- return 0;
-}
-
-// Only when we are done sending the last chunk of data do we check for
-// any trailing headers that are to be sent. This is the only opportunity
-// we have to make this check. If there are trailers, then the
-// NGHTTP2_DATA_FLAG_NO_END_STREAM flag must be set.
-inline void Nghttp2Session::GetTrailers(nghttp2_session* session,
- Nghttp2Session* handle,
- Nghttp2Stream* stream,
- uint32_t* flags) {
- if (stream->GetTrailers()) {
- SubmitTrailers submit_trailers{handle, stream, flags};
- handle->OnTrailers(stream, submit_trailers);
- }
-}
-
-// Submits any trailing header fields that have been collected
-inline void Nghttp2Session::SubmitTrailers::Submit(nghttp2_nv* trailers,
- size_t length) const {
- if (length == 0)
- return;
- DEBUG_HTTP2("Nghttp2Session %s: sending trailers for stream %d, "
- "count: %d\n", handle_->TypeName(),
- stream_->id(), length);
- *flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
- nghttp2_submit_trailer(handle_->session_,
- stream_->id(),
- trailers,
- length);
-}
-
-// Submits a graceful shutdown notice to nghttp
-// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html
-inline void Nghttp2Session::SubmitShutdownNotice() {
- DEBUG_HTTP2("Nghttp2Session %s: submitting shutdown notice\n",
- TypeName());
- nghttp2_submit_shutdown_notice(session_);
-}
-
-// Sends a SETTINGS frame on the current session
-// Note that this *should* send a SETTINGS frame even if niv == 0 and there
-// are no settings entries to send.
-inline int Nghttp2Session::SubmitSettings(const nghttp2_settings_entry iv[],
- size_t niv) {
- DEBUG_HTTP2("Nghttp2Session %s: submitting settings, count: %d\n",
- TypeName(), niv);
- return nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv);
-}
-
-// Returns the Nghttp2Stream associated with the given id, or nullptr if none
-inline Nghttp2Stream* Nghttp2Session::FindStream(int32_t id) {
- auto s = streams_.find(id);
- if (s != streams_.end()) {
- DEBUG_HTTP2("Nghttp2Session %s: stream %d found\n",
- TypeName(), id);
- return s->second;
- } else {
- DEBUG_HTTP2("Nghttp2Session %s: stream %d not found\n", TypeName(), id);
- return nullptr;
- }
-}
-
-// Flushes one buffered data chunk at a time.
-inline void Nghttp2Stream::FlushDataChunks() {
- if (!data_chunks_.empty()) {
- uv_buf_t buf = data_chunks_.front();
- data_chunks_.pop();
- if (buf.len > 0) {
- nghttp2_session_consume_stream(session_->session(), id_, buf.len);
- session_->OnDataChunk(this, &buf);
- } else {
- session_->OnDataChunk(this, nullptr);
- }
- }
-}
-
-// Called when a DATA frame has been completely processed. Will check to
-// see if the END_STREAM flag is set, and will flush the queued data chunks
-// to JS if the stream is flowing
-inline void Nghttp2Session::HandleDataFrame(const nghttp2_frame* frame) {
- int32_t id = GetFrameID(frame);
- DEBUG_HTTP2("Nghttp2Session %s: handling data frame for stream %d\n",
- TypeName(), id);
- Nghttp2Stream* stream = this->FindStream(id);
- // If the stream does not exist, something really bad happened
-#if defined(DEBUG) && DEBUG
- CHECK_NE(stream, nullptr);
-#endif
- if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)
- stream->data_chunks_.emplace(uv_buf_init(0, 0));
- if (stream->IsReading())
- stream->FlushDataChunks();
-}
-
-// Passes all of the collected headers for a HEADERS frame out to the JS layer.
-// The headers are collected as the frame is being processed and sent out
-// to the JS side only when the frame is fully processed.
-inline void Nghttp2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
- int32_t id = GetFrameID(frame);
- DEBUG_HTTP2("Nghttp2Session %s: handling headers frame for stream %d\n",
- TypeName(), id);
- Nghttp2Stream* stream = FindStream(id);
- // If the stream does not exist, something really bad happened
-#if defined(DEBUG) && DEBUG
- CHECK_NE(stream, nullptr);
-#endif
- OnHeaders(stream,
- stream->headers(),
- stream->headers_count(),
- stream->headers_category(),
- frame->hd.flags);
-}
-
-// Notifies the JS layer that a PRIORITY frame has been received
-inline void Nghttp2Session::HandlePriorityFrame(const nghttp2_frame* frame) {
- nghttp2_priority priority_frame = frame->priority;
- int32_t id = GetFrameID(frame);
- DEBUG_HTTP2("Nghttp2Session %s: handling priority frame for stream %d\n",
- TypeName(), id);
-
- // Priority frame stream ID should never be <= 0. nghttp2 handles this
- // as an error condition that terminates the session, so we should be
- // good here
-
-#if defined(DEBUG) && DEBUG
- CHECK_GT(id, 0);
-#endif
-
- nghttp2_priority_spec spec = priority_frame.pri_spec;
- OnPriority(id, spec.stream_id, spec.weight, spec.exclusive);
-}
-
-// Notifies the JS layer that a GOAWAY frame has been received
-inline void Nghttp2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
- nghttp2_goaway goaway_frame = frame->goaway;
- DEBUG_HTTP2("Nghttp2Session %s: handling goaway frame\n", TypeName());
-
- OnGoAway(goaway_frame.last_stream_id,
- goaway_frame.error_code,
- goaway_frame.opaque_data,
- goaway_frame.opaque_data_len);
-}
-
-// Prompts nghttp2 to flush the queue of pending data frames
-inline void Nghttp2Session::SendPendingData() {
- DEBUG_HTTP2("Nghttp2Session %s: Sending pending data\n", TypeName());
- // Do not attempt to send data on the socket if the destroying flag has
- // been set. That means everything is shutting down and the socket
- // will not be usable.
- if (IsDestroying())
- return;
-
- WriteWrap* req = nullptr;
- char* dest = nullptr;
- size_t destRemaining = 0;
- size_t destLength = 0; // amount of data stored in dest
- size_t destOffset = 0; // current write offset of dest
-
- const uint8_t* src; // pointer to the serialized data
- ssize_t srcLength = 0; // length of serialized data chunk
-
- // While srcLength is greater than zero
- while ((srcLength = nghttp2_session_mem_send(session_, &src)) > 0) {
- if (req == nullptr) {
- req = AllocateSend();
- destRemaining = req->ExtraSize();
- dest = req->Extra();
- }
- DEBUG_HTTP2("Nghttp2Session %s: nghttp2 has %d bytes to send\n",
- TypeName(), srcLength);
- size_t srcRemaining = srcLength;
- size_t srcOffset = 0;
-
- // The amount of data we have to copy is greater than the space
- // remaining. Copy what we can into the remaining space, send it,
- // the proceed with the rest.
- while (srcRemaining > destRemaining) {
- DEBUG_HTTP2("Nghttp2Session %s: pushing %d bytes to the socket\n",
- TypeName(), destLength + destRemaining);
- memcpy(dest + destOffset, src + srcOffset, destRemaining);
- destLength += destRemaining;
- Send(req, dest, destLength);
- destOffset = 0;
- destLength = 0;
- srcRemaining -= destRemaining;
- srcOffset += destRemaining;
- req = AllocateSend();
- destRemaining = req->ExtraSize();
- dest = req->Extra();
- }
-
- if (srcRemaining > 0) {
- memcpy(dest + destOffset, src + srcOffset, srcRemaining);
- destLength += srcRemaining;
- destOffset += srcRemaining;
- destRemaining -= srcRemaining;
- srcRemaining = 0;
- srcOffset = 0;
- }
- }
-
- if (destLength > 0) {
- DEBUG_HTTP2("Nghttp2Session %s: pushing %d bytes to the socket\n",
- TypeName(), destLength);
- Send(req, dest, destLength);
- }
-}
-
-// Initialize the Nghttp2Session handle by creating and
-// assigning the Nghttp2Session instance and associated
-// uv_loop_t.
-inline int Nghttp2Session::Init(const nghttp2_session_type type,
- nghttp2_option* options,
- nghttp2_mem* mem,
- uint32_t maxHeaderPairs) {
- session_type_ = type;
- DEBUG_HTTP2("Nghttp2Session %s: initializing session\n", TypeName());
- destroying_ = false;
-
- max_header_pairs_ = maxHeaderPairs;
-
- nghttp2_session_callbacks* callbacks
- = callback_struct_saved[HasGetPaddingCallback() ? 1 : 0].callbacks;
-
- CHECK_NE(options, nullptr);
-
- typedef int (*init_fn)(nghttp2_session** session,
- const nghttp2_session_callbacks* callbacks,
- void* user_data,
- const nghttp2_option* options,
- nghttp2_mem* mem);
- init_fn fn = type == NGHTTP2_SESSION_SERVER ?
- nghttp2_session_server_new3 :
- nghttp2_session_client_new3;
-
- return fn(&session_, callbacks, this, options, mem);
-}
-
-inline void Nghttp2Session::MarkDestroying() {
- destroying_ = true;
-}
-
-inline Nghttp2Session::~Nghttp2Session() {
- Close();
-}
-
-inline void Nghttp2Session::Close() {
- if (IsClosed())
- return;
- DEBUG_HTTP2("Nghttp2Session %s: freeing session\n", TypeName());
- nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR);
- nghttp2_session_del(session_);
- session_ = nullptr;
- DEBUG_HTTP2("Nghttp2Session %s: session freed\n", TypeName());
-}
-
-// Write data received from the socket to the underlying nghttp2_session.
-inline ssize_t Nghttp2Session::Write(const uv_buf_t* bufs, unsigned int nbufs) {
- size_t total = 0;
- for (unsigned int n = 0; n < nbufs; n++) {
- ssize_t ret =
- nghttp2_session_mem_recv(session_,
- reinterpret_cast<uint8_t*>(bufs[n].base),
- bufs[n].len);
- if (ret < 0) {
- return ret;
- } else {
- total += ret;
- }
- }
- SendPendingData();
- return total;
-}
-
-inline void Nghttp2Session::AddStream(Nghttp2Stream* stream) {
- streams_[stream->id()] = stream;
-}
-
-// Removes a stream instance from this session
-inline void Nghttp2Session::RemoveStream(int32_t id) {
- streams_.erase(id);
-}
-
-// Implementation for Nghttp2Stream functions
-
-Nghttp2Stream::Nghttp2Stream(
- int32_t id,
- Nghttp2Session* session,
- nghttp2_headers_category category,
- int options) : id_(id),
- session_(session),
- current_headers_category_(category) {
- // Limit the number of header pairs
- max_header_pairs_ = session->GetMaxHeaderPairs();
- if (max_header_pairs_ == 0)
- max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
- current_headers_.reserve(max_header_pairs_);
-
- // Limit the number of header octets
- max_header_length_ =
- std::min(
- nghttp2_session_get_local_settings(
- session->session(),
- NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE),
- MAX_MAX_HEADER_LIST_SIZE);
-
- getTrailers_ = options & STREAM_OPTION_GET_TRAILERS;
- if (options & STREAM_OPTION_EMPTY_PAYLOAD)
- Shutdown();
- session->AddStream(this);
-}
-
-
-inline void Nghttp2Stream::Destroy() {
- DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_);
- // Do nothing if this stream instance is already destroyed
- if (IsDestroyed())
- return;
- flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED;
- Nghttp2Session* session = this->session_;
-
- if (session != nullptr) {
- session_->RemoveStream(this->id());
- session_ = nullptr;
- }
-
- // Free any remaining incoming data chunks.
- while (!data_chunks_.empty()) {
- uv_buf_t buf = data_chunks_.front();
- free(buf.base);
- data_chunks_.pop();
- }
-
- // Free any remaining outgoing data chunks.
- while (!queue_.empty()) {
- nghttp2_stream_write* head = queue_.front();
- head->cb(head->req, UV_ECANCELED);
- delete head;
- queue_.pop();
- }
-
- delete this;
-}
-
-// Submit informational headers for a stream.
-inline int Nghttp2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
- DEBUG_HTTP2("Nghttp2Stream %d: sending informational headers, count: %d\n",
- id_, len);
- CHECK_GT(len, 0);
- return nghttp2_submit_headers(session_->session(),
- NGHTTP2_FLAG_NONE,
- id_, nullptr,
- nva, len, nullptr);
-}
-
-inline int Nghttp2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
- bool silent) {
- DEBUG_HTTP2("Nghttp2Stream %d: sending priority spec\n", id_);
- return silent ?
- nghttp2_session_change_stream_priority(session_->session(),
- id_, prispec) :
- nghttp2_submit_priority(session_->session(),
- NGHTTP2_FLAG_NONE,
- id_, prispec);
-}
-
-// Submit an RST_STREAM frame
-inline int Nghttp2Stream::SubmitRstStream(const uint32_t code) {
- DEBUG_HTTP2("Nghttp2Stream %d: sending rst-stream, code: %d\n", id_, code);
- session_->SendPendingData();
- return nghttp2_submit_rst_stream(session_->session(),
- NGHTTP2_FLAG_NONE,
- id_,
- code);
-}
-
-// Submit a push promise.
-inline int32_t Nghttp2Stream::SubmitPushPromise(
- nghttp2_nv* nva,
- size_t len,
- Nghttp2Stream** assigned,
- int options) {
-#if defined(DEBUG) && DEBUG
- CHECK_GT(len, 0);
-#endif
- DEBUG_HTTP2("Nghttp2Stream %d: sending push promise\n", id_);
- int32_t ret = nghttp2_submit_push_promise(session_->session(),
- NGHTTP2_FLAG_NONE,
- id_, nva, len,
- nullptr);
- if (ret > 0) {
- auto stream = new Nghttp2Stream(ret, session_,
- NGHTTP2_HCAT_HEADERS,
- options);
- if (assigned != nullptr) *assigned = stream;
- }
- return ret;
-}
-
-// Initiate a response. If the nghttp2_stream is still writable by
-// the time this is called, then an nghttp2_data_provider will be
-// initialized, causing at least one (possibly empty) data frame to
-// be sent.
-inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva,
- size_t len,
- int options) {
-#if defined(DEBUG) && DEBUG
- CHECK_GT(len, 0);
-#endif
- DEBUG_HTTP2("Nghttp2Stream %d: submitting response\n", id_);
- getTrailers_ = options & STREAM_OPTION_GET_TRAILERS;
- nghttp2_data_provider* provider = nullptr;
- nghttp2_data_provider prov;
- prov.source.ptr = this;
- prov.read_callback = Nghttp2Session::OnStreamRead;
- if (IsWritable() && !(options & STREAM_OPTION_EMPTY_PAYLOAD))
- provider = &prov;
-
- return nghttp2_submit_response(session_->session(), id_,
- nva, len, provider);
-}
-
-// Initiate a response that contains data read from a file descriptor.
-inline int Nghttp2Stream::SubmitFile(int fd,
- nghttp2_nv* nva, size_t len,
- int64_t offset,
- int64_t length,
- int options) {
-#if defined(DEBUG) && DEBUG
- CHECK_GT(len, 0);
- CHECK_GT(fd, 0);
-#endif
- DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_);
- getTrailers_ = options & STREAM_OPTION_GET_TRAILERS;
- nghttp2_data_provider prov;
- prov.source.fd = fd;
- prov.read_callback = Nghttp2Session::OnStreamReadFD;
-
- if (offset > 0) fd_offset_ = offset;
- if (length > -1) fd_length_ = length;
-
- return nghttp2_submit_response(session_->session(), id_,
- nva, len, &prov);
-}
-
-// Initiate a request. If writable is true (the default), then
-// an nghttp2_data_provider will be initialized, causing at
-// least one (possibly empty) data frame to to be sent.
-inline int32_t Nghttp2Session::SubmitRequest(
- nghttp2_priority_spec* prispec,
- nghttp2_nv* nva,
- size_t len,
- Nghttp2Stream** assigned,
- int options) {
-#if defined(DEBUG) && DEBUG
- CHECK_GT(len, 0);
-#endif
- DEBUG_HTTP2("Nghttp2Session: submitting request\n");
- nghttp2_data_provider* provider = nullptr;
- nghttp2_data_provider prov;
- prov.source.ptr = this;
- prov.read_callback = OnStreamRead;
- if (!(options & STREAM_OPTION_EMPTY_PAYLOAD))
- provider = &prov;
- int32_t ret = nghttp2_submit_request(session_,
- prispec, nva, len,
- provider, nullptr);
- // Assign the Nghttp2Stream handle
- if (ret > 0) {
- auto stream = new Nghttp2Stream(ret, this, NGHTTP2_HCAT_HEADERS, options);
- if (assigned != nullptr) *assigned = stream;
- }
- return ret;
-}
-
-// Queue the given set of uv_but_t handles for writing to an
-// nghttp2_stream. The callback will be invoked once the chunks
-// of data have been flushed to the underlying nghttp2_session.
-// Note that this does *not* mean that the data has been flushed
-// to the socket yet.
-inline int Nghttp2Stream::Write(nghttp2_stream_write_t* req,
- const uv_buf_t bufs[],
- unsigned int nbufs,
- nghttp2_stream_write_cb cb) {
- if (!IsWritable()) {
- if (cb != nullptr)
- cb(req, UV_EOF);
- return 0;
- }
- DEBUG_HTTP2("Nghttp2Stream %d: queuing buffers to send, count: %d\n",
- id_, nbufs);
- nghttp2_stream_write* item = new nghttp2_stream_write;
- item->cb = cb;
- item->req = req;
- item->nbufs = nbufs;
- item->bufs.AllocateSufficientStorage(nbufs);
- memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs));
- queue_.push(item);
- nghttp2_session_resume_data(session_->session(), id_);
- return 0;
-}
-
-inline void Nghttp2Stream::ReadStart() {
- if (IsReading())
- return;
- DEBUG_HTTP2("Nghttp2Stream %d: start reading\n", id_);
- flags_ |= NGHTTP2_STREAM_FLAG_READ_START;
- flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED;
-
- // Flush any queued data chunks immediately out to the JS layer
- FlushDataChunks();
-}
-
-inline void Nghttp2Stream::ReadResume() {
- DEBUG_HTTP2("Nghttp2Stream %d: resume reading\n", id_);
- flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED;
-
- // Flush any queued data chunks immediately out to the JS layer
- FlushDataChunks();
-}
-
-inline void Nghttp2Stream::ReadStop() {
- DEBUG_HTTP2("Nghttp2Stream %d: stop reading\n", id_);
- if (!IsReading())
- return;
- flags_ |= NGHTTP2_STREAM_FLAG_READ_PAUSED;
-}
-
-Nghttp2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) {
- nghttp2_session_callbacks_new(&callbacks);
- nghttp2_session_callbacks_set_on_begin_headers_callback(
- callbacks, OnBeginHeadersCallback);
- nghttp2_session_callbacks_set_on_header_callback2(
- callbacks, OnHeaderCallback);
- nghttp2_session_callbacks_set_on_frame_recv_callback(
- callbacks, OnFrameReceive);
- nghttp2_session_callbacks_set_on_stream_close_callback(
- callbacks, OnStreamClose);
- nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
- callbacks, OnDataChunkReceived);
- nghttp2_session_callbacks_set_on_frame_not_send_callback(
- callbacks, OnFrameNotSent);
- nghttp2_session_callbacks_set_on_invalid_header_callback2(
- callbacks, OnInvalidHeader);
-
-#ifdef NODE_DEBUG_HTTP2
- nghttp2_session_callbacks_set_error_callback(
- callbacks, OnNghttpError);
-#endif
-
- if (kHasGetPaddingCallback) {
- nghttp2_session_callbacks_set_select_padding_callback(
- callbacks, OnSelectPadding);
- }
-}
-
-Nghttp2Session::Callbacks::~Callbacks() {
- nghttp2_session_callbacks_del(callbacks);
-}
-
-Nghttp2Session::SubmitTrailers::SubmitTrailers(
- Nghttp2Session* handle,
- Nghttp2Stream* stream,
- uint32_t* flags)
- : handle_(handle), stream_(stream), flags_(flags) { }
-
-} // namespace http2
-} // namespace node
-
-#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
-
-#endif // SRC_NODE_HTTP2_CORE_INL_H_
diff --git a/src/node_http2_core.h b/src/node_http2_core.h
deleted file mode 100644
index 5fbb7fa9f2..0000000000
--- a/src/node_http2_core.h
+++ /dev/null
@@ -1,516 +0,0 @@
-#ifndef SRC_NODE_HTTP2_CORE_H_
-#define SRC_NODE_HTTP2_CORE_H_
-
-#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
-
-#include "stream_base.h"
-#include "util-inl.h"
-#include "uv.h"
-#include "nghttp2/nghttp2.h"
-
-#include <queue>
-#include <vector>
-#include <stdio.h>
-#include <unordered_map>
-
-namespace node {
-namespace http2 {
-
-#ifdef NODE_DEBUG_HTTP2
-
-// Adapted from nghttp2 own debug printer
-static inline void _debug_vfprintf(const char* fmt, va_list args) {
- vfprintf(stderr, fmt, args);
-}
-
-void inline debug_vfprintf(const char* format, ...) {
- va_list args;
- va_start(args, format);
- _debug_vfprintf(format, args);
- va_end(args);
-}
-
-#define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__);
-#else
-#define DEBUG_HTTP2(...) \
- do { \
- } while (0)
-#endif
-
-#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
-#define DEFAULT_SETTINGS_ENABLE_PUSH 1
-#define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535
-#define DEFAULT_SETTINGS_MAX_FRAME_SIZE 16384
-#define DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE 65535
-#define MAX_MAX_FRAME_SIZE 16777215
-#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE
-#define MAX_INITIAL_WINDOW_SIZE 2147483647
-
-#define MAX_MAX_HEADER_LIST_SIZE 16777215u
-#define DEFAULT_MAX_HEADER_LIST_PAIRS 128u
-
-
-class Nghttp2Session;
-class Nghttp2Stream;
-
-struct nghttp2_stream_write_t;
-
-#define MAX_BUFFER_COUNT 16
-
-enum nghttp2_session_type {
- NGHTTP2_SESSION_SERVER,
- NGHTTP2_SESSION_CLIENT
-};
-
-enum nghttp2_shutdown_flags {
- NGHTTP2_SHUTDOWN_FLAG_GRACEFUL
-};
-
-enum nghttp2_stream_flags {
- NGHTTP2_STREAM_FLAG_NONE = 0x0,
- // Writable side has ended
- NGHTTP2_STREAM_FLAG_SHUT = 0x1,
- // Reading has started
- NGHTTP2_STREAM_FLAG_READ_START = 0x2,
- // Reading is paused
- NGHTTP2_STREAM_FLAG_READ_PAUSED = 0x4,
- // Stream is closed
- NGHTTP2_STREAM_FLAG_CLOSED = 0x8,
- // Stream is destroyed
- NGHTTP2_STREAM_FLAG_DESTROYED = 0x10
-};
-
-enum nghttp2_stream_options {
- STREAM_OPTION_EMPTY_PAYLOAD = 0x1,
- STREAM_OPTION_GET_TRAILERS = 0x2,
-};
-
-// Callbacks
-typedef void (*nghttp2_stream_write_cb)(
- nghttp2_stream_write_t* req,
- int status);
-
-struct nghttp2_stream_write {
- unsigned int nbufs = 0;
- nghttp2_stream_write_t* req = nullptr;
- nghttp2_stream_write_cb cb = nullptr;
- MaybeStackBuffer<uv_buf_t, MAX_BUFFER_COUNT> bufs;
-};
-
-struct nghttp2_header {
- nghttp2_rcbuf* name = nullptr;
- nghttp2_rcbuf* value = nullptr;
- uint8_t flags = 0;
-};
-
-// Handle Types
-class Nghttp2Session {
- public:
- // Initializes the session instance
- inline int Init(
- const nghttp2_session_type type = NGHTTP2_SESSION_SERVER,
- nghttp2_option* options = nullptr,
- nghttp2_mem* mem = nullptr,
- uint32_t maxHeaderPairs = DEFAULT_MAX_HEADER_LIST_PAIRS);
-
- // Frees this session instance
- inline ~Nghttp2Session();
- inline void MarkDestroying();
- bool IsDestroying() {
- return destroying_;
- }
-
- uint32_t GetMaxHeaderPairs() const {
- return max_header_pairs_;
- }
-
- inline const char* TypeName() {
- switch (session_type_) {
- case NGHTTP2_SESSION_SERVER: return "server";
- case NGHTTP2_SESSION_CLIENT: return "client";
- default:
- // This should never happen
- ABORT();
- }
- }
-
- // Returns the pointer to the identified stream, or nullptr if
- // the stream does not exist
- inline Nghttp2Stream* FindStream(int32_t id);
-
- // Submits a new request. If the request is a success, assigned
- // will be a pointer to the Nghttp2Stream instance assigned.
- // This only works if the session is a client session.
- inline int32_t SubmitRequest(
- nghttp2_priority_spec* prispec,
- nghttp2_nv* nva,
- size_t len,
- Nghttp2Stream** assigned = nullptr,
- int options = 0);
-
- // Submits a notice to the connected peer that the session is in the
- // process of shutting down.
- inline void SubmitShutdownNotice();
-
- // Submits a SETTINGS frame to the connected peer.
- inline int SubmitSettings(const nghttp2_settings_entry iv[], size_t niv);
-
- // Write data to the session
- inline ssize_t Write(const uv_buf_t* bufs, unsigned int nbufs);
-
- // Returns the nghttp2 library session
- inline nghttp2_session* session() const { return session_; }
-
- inline bool IsClosed() const { return session_ == nullptr; }
-
- nghttp2_session_type type() const {
- return session_type_;
- }
-
- protected:
- // Adds a stream instance to this session
- inline void AddStream(Nghttp2Stream* stream);
-
- // Removes a stream instance from this session
- inline void RemoveStream(int32_t id);
-
- virtual void OnHeaders(
- Nghttp2Stream* stream,
- nghttp2_header* headers,
- size_t count,
- nghttp2_headers_category cat,
- uint8_t flags) {}
- virtual void OnStreamClose(int32_t id, uint32_t code) {}
- virtual void OnDataChunk(Nghttp2Stream* stream,
- uv_buf_t* chunk) {}
- virtual void OnSettings(bool ack) {}
- virtual void OnPriority(int32_t id,
- int32_t parent,
- int32_t weight,
- int8_t exclusive) {}
- virtual void OnGoAway(int32_t lastStreamID,
- uint32_t errorCode,
- uint8_t* data,
- size_t length) {}
- virtual void OnFrameError(int32_t id,
- uint8_t type,
- int error_code) {}
- virtual ssize_t GetPadding(size_t frameLength,
- size_t maxFrameLength) { return 0; }
-
- inline void SendPendingData();
- virtual void Send(WriteWrap* req, char* buf, size_t length) = 0;
- virtual WriteWrap* AllocateSend() = 0;
-
- virtual bool HasGetPaddingCallback() { return false; }
-
- class SubmitTrailers {
- public:
- inline void Submit(nghttp2_nv* trailers, size_t length) const;
-
- private:
- inline SubmitTrailers(Nghttp2Session* handle,
- Nghttp2Stream* stream,
- uint32_t* flags);
-
- Nghttp2Session* const handle_;
- Nghttp2Stream* const stream_;
- uint32_t* const flags_;
-
- friend class Nghttp2Session;
- };
-
- virtual void OnTrailers(Nghttp2Stream* stream,
- const SubmitTrailers& submit_trailers) {}
-
- virtual uv_loop_t* event_loop() const = 0;
-
- virtual void Close();
-
- private:
- inline void HandleHeadersFrame(const nghttp2_frame* frame);
- inline void HandlePriorityFrame(const nghttp2_frame* frame);
- inline void HandleDataFrame(const nghttp2_frame* frame);
- inline void HandleGoawayFrame(const nghttp2_frame* frame);
-
- static inline void GetTrailers(nghttp2_session* session,
- Nghttp2Session* handle,
- Nghttp2Stream* stream,
- uint32_t* flags);
-
- /* callbacks for nghttp2 */
-#ifdef NODE_DEBUG_HTTP2
- static inline int OnNghttpError(nghttp2_session* session,
- const char* message,
- size_t len,
- void* user_data);
-#endif
-
- static inline int OnBeginHeadersCallback(nghttp2_session* session,
- const nghttp2_frame* frame,
- void* user_data);
- static inline int OnHeaderCallback(nghttp2_session* session,
- const nghttp2_frame* frame,
- nghttp2_rcbuf* name,
- nghttp2_rcbuf* value,
- uint8_t flags,
- void* user_data);
- static inline int OnFrameReceive(nghttp2_session* session,
- const nghttp2_frame* frame,
- void* user_data);
- static inline int OnFrameNotSent(nghttp2_session* session,
- const nghttp2_frame* frame,
- int error_code,
- void* user_data);
- static inline int OnStreamClose(nghttp2_session* session,
- int32_t id,
- uint32_t code,
- void* user_data);
- static inline int OnInvalidHeader(nghttp2_session* session,
- const nghttp2_frame* frame,
- nghttp2_rcbuf* name,
- nghttp2_rcbuf* value,
- uint8_t flags,
- void* user_data);
- static inline int OnDataChunkReceived(nghttp2_session* session,
- uint8_t flags,
- int32_t id,
- const uint8_t* data,
- size_t len,
- void* user_data);
- static inline ssize_t OnStreamReadFD(nghttp2_session* session,
- int32_t id,
- uint8_t* buf,
- size_t length,
- uint32_t* flags,
- nghttp2_data_source* source,
- void* user_data);
- static inline ssize_t OnStreamRead(nghttp2_session* session,
- int32_t id,
- uint8_t* buf,
- size_t length,
- uint32_t* flags,
- nghttp2_data_source* source,
- void* user_data);
- static inline ssize_t OnSelectPadding(nghttp2_session* session,
- const nghttp2_frame* frame,
- size_t maxPayloadLen,
- void* user_data);
-
- struct Callbacks {
- inline explicit Callbacks(bool kHasGetPaddingCallback);
- inline ~Callbacks();
-
- nghttp2_session_callbacks* callbacks;
- };
-
- /* Use callback_struct_saved[kHasGetPaddingCallback ? 1 : 0] */
- static Callbacks callback_struct_saved[2];
-
- nghttp2_session* session_;
- nghttp2_session_type session_type_;
- uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
- std::unordered_map<int32_t, Nghttp2Stream*> streams_;
- bool destroying_ = false;
-
- friend class Nghttp2Stream;
-};
-
-
-
-class Nghttp2Stream {
- public:
- // Resets the state of the stream instance to defaults
- Nghttp2Stream(
- int32_t id,
- Nghttp2Session* session,
- nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS,
- int options = 0);
-
- inline ~Nghttp2Stream() {}
-
- inline void FlushDataChunks();
-
- // Destroy this stream instance and free all held memory.
- // Note that this will free queued outbound and inbound
- // data chunks and inbound headers, so it's important not
- // to call this until those are fully consumed.
- inline void Destroy();
-
- // Returns true if this stream has been destroyed
- inline bool IsDestroyed() const {
- return flags_ & NGHTTP2_STREAM_FLAG_DESTROYED;
- }
-
- // Queue outbound chunks of data to be sent on this stream
- inline int Write(
- nghttp2_stream_write_t* req,
- const uv_buf_t bufs[],
- unsigned int nbufs,
- nghttp2_stream_write_cb cb);
-
- // Initiate a response on this stream.
- inline int SubmitResponse(nghttp2_nv* nva,
- size_t len,
- int options);
-
- // Send data read from a file descriptor as the response on this stream.
- inline int SubmitFile(int fd,
- nghttp2_nv* nva, size_t len,
- int64_t offset,
- int64_t length,
- int options);
-
- // Submit informational headers for this stream
- inline int SubmitInfo(nghttp2_nv* nva, size_t len);
-
- // Submit a PRIORITY frame for this stream
- inline int SubmitPriority(nghttp2_priority_spec* prispec,
- bool silent = false);
-
- // Submits an RST_STREAM frame using the given code
- inline int SubmitRstStream(const uint32_t code);
-
- // Submits a PUSH_PROMISE frame with this stream as the parent.
- inline int SubmitPushPromise(
- nghttp2_nv* nva,
- size_t len,
- Nghttp2Stream** assigned = nullptr,
- int options = 0);
-
- // Marks the Writable side of the stream as being shutdown
- inline void Shutdown() {
- flags_ |= NGHTTP2_STREAM_FLAG_SHUT;
- nghttp2_session_resume_data(session_->session(), id_);
- }
-
- // Returns true if this stream is writable.
- inline bool IsWritable() const {
- return !(flags_ & NGHTTP2_STREAM_FLAG_SHUT);
- }
-
- // Start Reading. If there are queued data chunks, they are pushed into
- // the session to be emitted at the JS side
- inline void ReadStart();
-
- // Resume Reading
- inline void ReadResume();
-
- // Stop/Pause Reading.
- inline void ReadStop();
-
- // Returns true if reading is paused
- inline bool IsPaused() const {
- return flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED;
- }
-
- inline bool GetTrailers() const {
- return getTrailers_;
- }
-
- // Returns true if this stream is in the reading state, which occurs when
- // the NGHTTP2_STREAM_FLAG_READ_START flag has been set and the
- // NGHTTP2_STREAM_FLAG_READ_PAUSED flag is *not* set.
- inline bool IsReading() const {
- return flags_ & NGHTTP2_STREAM_FLAG_READ_START &&
- !(flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED);
- }
-
- inline void Close(int32_t code) {
- DEBUG_HTTP2("Nghttp2Stream %d: closing with code %d\n", id_, code);
- flags_ |= NGHTTP2_STREAM_FLAG_CLOSED;
- code_ = code;
- session_->OnStreamClose(id_, code);
- DEBUG_HTTP2("Nghttp2Stream %d: closed\n", id_);
- }
-
- // Returns true if this stream has been closed either by receiving or
- // sending an RST_STREAM frame.
- inline bool IsClosed() const {
- return flags_ & NGHTTP2_STREAM_FLAG_CLOSED;
- }
-
- // Returns the RST_STREAM code used to close this stream
- inline int32_t code() const {
- return code_;
- }
-
- // Returns the stream identifier for this stream
- inline int32_t id() const {
- return id_;
- }
-
- inline bool AddHeader(nghttp2_rcbuf* name,
- nghttp2_rcbuf* value,
- uint8_t flags);
-
- inline nghttp2_header* headers() {
- return current_headers_.data();
- }
-
- inline nghttp2_headers_category headers_category() const {
- return current_headers_category_;
- }
-
- inline size_t headers_count() const {
- return current_headers_.size();
- }
-
- void StartHeaders(nghttp2_headers_category category) {
- DEBUG_HTTP2("Nghttp2Stream %d: starting headers, category: %d\n",
- id_, category);
- current_headers_length_ = 0;
- current_headers_.clear();
- current_headers_category_ = category;
- }
-
- private:
- // The Stream Identifier
- int32_t id_;
-
- // The Parent HTTP/2 Session
- Nghttp2Session* session_;
-
- // Internal state flags
- int flags_ = NGHTTP2_STREAM_FLAG_NONE;
- uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
- uint32_t max_header_length_ = DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE;
-
- // The RST_STREAM code used to close this stream
- int32_t code_ = NGHTTP2_NO_ERROR;
-
- // Outbound Data... This is the data written by the JS layer that is
- // waiting to be written out to the socket.
- std::queue<nghttp2_stream_write*> queue_;
- unsigned int queue_index_ = 0;
- size_t queue_offset_ = 0;
- int64_t fd_offset_ = 0;
- int64_t fd_length_ = -1;
-
- // True if this stream will have outbound trailers
- bool getTrailers_ = false;
-
- // The Current Headers block... As headers are received for this stream,
- // they are temporarily stored here until the OnFrameReceived is called
- // signalling the end of the HEADERS frame
- nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS;
- uint32_t current_headers_length_ = 0; // total number of octets
- std::vector<nghttp2_header> current_headers_;
-
- // Inbound Data... This is the data received via DATA frames for this stream.
- std::queue<uv_buf_t> data_chunks_;
-
- friend class Nghttp2Session;
-};
-
-struct nghttp2_stream_write_t {
- void* data;
- int status;
-};
-
-} // namespace http2
-} // namespace node
-
-#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
-
-#endif // SRC_NODE_HTTP2_CORE_H_
diff --git a/src/node_http2_state.h b/src/node_http2_state.h
index dd8954de2a..a7ad23fb51 100755
--- a/src/node_http2_state.h
+++ b/src/node_http2_state.h
@@ -48,6 +48,7 @@ namespace http2 {
IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS,
IDX_OPTIONS_PADDING_STRATEGY,
IDX_OPTIONS_MAX_HEADER_LIST_PAIRS,
+ IDX_OPTIONS_MAX_OUTSTANDING_PINGS,
IDX_OPTIONS_FLAGS
};
diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js
index 569979e73e..4366502963 100644
--- a/test/parallel/test-http2-client-data-end.js
+++ b/test/parallel/test-http2-client-data-end.js
@@ -82,7 +82,7 @@ server.listen(0, common.mustCall(() => {
let data = '';
req.setEncoding('utf8');
- req.on('data', common.mustCall((d) => data += d));
+ req.on('data', common.mustCallAtLeast((d) => data += d));
req.on('end', common.mustCall(() => {
assert.strictEqual(data, 'test');
maybeClose();
diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js
index 8b91f2d210..bb93366247 100644
--- a/test/parallel/test-http2-client-destroy.js
+++ b/test/parallel/test-http2-client-destroy.js
@@ -95,7 +95,6 @@ const { kSocket } = require('internal/http2/util');
common.expectsError(() => client.request(), sessionError);
common.expectsError(() => client.settings({}), sessionError);
- common.expectsError(() => client.priority(req, {}), sessionError);
common.expectsError(() => client.shutdown(), sessionError);
// Wait for setImmediate call from destroy() to complete
@@ -103,9 +102,7 @@ const { kSocket } = require('internal/http2/util');
setImmediate(() => {
common.expectsError(() => client.request(), sessionError);
common.expectsError(() => client.settings({}), sessionError);
- common.expectsError(() => client.priority(req, {}), sessionError);
common.expectsError(() => client.shutdown(), sessionError);
- common.expectsError(() => client.rstStream(req), sessionError);
});
req.on(
diff --git a/test/parallel/test-http2-client-http1-server.js b/test/parallel/test-http2-client-http1-server.js
new file mode 100644
index 0000000000..44d8d392f4
--- /dev/null
+++ b/test/parallel/test-http2-client-http1-server.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const http = require('http');
+const http2 = require('http2');
+
+const server = http.createServer(common.mustNotCall());
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request();
+ req.on('streamClosed', common.mustCall());
+
+ client.on('error', common.expectsError({
+ code: 'ERR_HTTP2_ERROR',
+ type: Error,
+ message: 'Protocol error'
+ }));
+
+ client.on('close', (...args) => {
+ server.close();
+ });
+}));
diff --git a/test/parallel/test-http2-client-onconnect-errors.js b/test/parallel/test-http2-client-onconnect-errors.js
index 51ceb83677..0800775365 100644
--- a/test/parallel/test-http2-client-onconnect-errors.js
+++ b/test/parallel/test-http2-client-onconnect-errors.js
@@ -11,28 +11,17 @@ if (!common.hasCrypto)
const http2 = require('http2');
// tests error handling within requestOnConnect
-// - NGHTTP2_ERR_NOMEM (should emit session error)
// - NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE (should emit session error)
// - NGHTTP2_ERR_INVALID_ARGUMENT (should emit stream error)
// - every other NGHTTP2 error from binding (should emit session error)
const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM',
'NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE',
'NGHTTP2_ERR_INVALID_ARGUMENT'
];
const specificTests = [
{
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- },
- type: 'session'
- },
- {
ngError: constants.NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
error: {
code: 'ERR_HTTP2_OUT_OF_STREAMS',
@@ -40,7 +29,7 @@ const specificTests = [
message: 'No stream ID is available because ' +
'maximum stream ID has been reached'
},
- type: 'session'
+ type: 'stream'
},
{
ngError: constants.NGHTTP2_ERR_INVALID_ARGUMENT,
@@ -72,24 +61,15 @@ const tests = specificTests.concat(genericTests);
let currentError;
// mock submitRequest because we only care about testing error handling
-Http2Session.prototype.submitRequest = () => currentError;
+Http2Session.prototype.request = () => currentError;
const server = http2.createServer(common.mustNotCall());
server.listen(0, common.mustCall(() => runTest(tests.shift())));
function runTest(test) {
- const port = server.address().port;
- const url = `http://localhost:${port}`;
- const headers = {
- ':path': '/',
- ':method': 'POST',
- ':scheme': 'http',
- ':authority': `localhost:${port}`
- };
-
- const client = http2.connect(url);
- const req = client.request(headers);
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request({ ':method': 'POST' });
currentError = test.ngError;
req.resume();
diff --git a/test/parallel/test-http2-client-priority-before-connect.js b/test/parallel/test-http2-client-priority-before-connect.js
index 7fc3841622..b062107e4a 100644
--- a/test/parallel/test-http2-client-priority-before-connect.js
+++ b/test/parallel/test-http2-client-priority-before-connect.js
@@ -25,7 +25,7 @@ server.on('listening', common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`);
const req = client.request({ ':path': '/' });
- client.priority(req, {});
+ req.priority({});
req.on('response', common.mustCall());
req.resume();
diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js
index 3c4ac3b34d..e4aff87be9 100644
--- a/test/parallel/test-http2-client-rststream-before-connect.js
+++ b/test/parallel/test-http2-client-rststream-before-connect.js
@@ -7,6 +7,10 @@ const assert = require('assert');
const h2 = require('http2');
const server = h2.createServer();
+server.on('stream', (stream) => {
+ stream.respond();
+ stream.end('ok');
+});
server.listen(0);
@@ -15,15 +19,13 @@ server.on('listening', common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`);
const req = client.request({ ':path': '/' });
- client.rstStream(req, 0);
- assert.strictEqual(req.rstCode, 0);
+ req.rstStream(0);
// make sure that destroy is called
req._destroy = common.mustCall(req._destroy.bind(req));
// second call doesn't do anything
- assert.doesNotThrow(() => client.rstStream(req, 8));
- assert.strictEqual(req.rstCode, 0);
+ assert.doesNotThrow(() => req.rstStream(8));
req.on('streamClosed', common.mustCall((code) => {
assert.strictEqual(req.destroyed, true);
diff --git a/test/parallel/test-http2-client-settings-errors.js b/test/parallel/test-http2-client-settings-errors.js
deleted file mode 100644
index d3a8ea9d8b..0000000000
--- a/test/parallel/test-http2-client-settings-errors.js
+++ /dev/null
@@ -1,84 +0,0 @@
-'use strict';
-
-const {
- constants,
- Http2Session,
- nghttp2ErrorString
-} = process.binding('http2');
-const common = require('../common');
-if (!common.hasCrypto)
- common.skip('missing crypto');
-const http2 = require('http2');
-
-// tests error handling within requestOnConnect
-// - NGHTTP2_ERR_NOMEM (should emit session error)
-// - every other NGHTTP2 error from binding (should emit session error)
-
-const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM'
-];
-
-const specificTests = [
- {
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- }
- }
-];
-
-const genericTests = Object.getOwnPropertyNames(constants)
- .filter((key) => (
- key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
- ))
- .map((key) => ({
- ngError: constants[key],
- error: {
- code: 'ERR_HTTP2_ERROR',
- type: Error,
- message: nghttp2ErrorString(constants[key])
- }
- }));
-
-const tests = specificTests.concat(genericTests);
-
-const server = http2.createServer(common.mustNotCall());
-server.on('sessionError', () => {}); // not being tested
-
-server.listen(0, common.mustCall(() => runTest(tests.shift())));
-
-function runTest(test) {
- // mock submitSettings because we only care about testing error handling
- Http2Session.prototype.submitSettings = () => test.ngError;
-
- const errorMustCall = common.expectsError(test.error);
- const errorMustNotCall = common.mustNotCall(
- `${test.error.code} should emit on session`
- );
-
- const url = `http://localhost:${server.address().port}`;
-
- const client = http2.connect(url, {
- settings: {
- maxHeaderListSize: 1
- }
- });
-
- const req = client.request();
- req.resume();
- req.end();
-
- client.on('error', errorMustCall);
- req.on('error', errorMustNotCall);
-
- req.on('end', common.mustCall(() => {
- client.destroy();
- if (!tests.length) {
- server.close();
- } else {
- runTest(tests.shift());
- }
- }));
-}
diff --git a/test/parallel/test-http2-compat-serverrequest-trailers.js b/test/parallel/test-http2-compat-serverrequest-trailers.js
index 713e4babd2..b4d9028191 100644
--- a/test/parallel/test-http2-compat-serverrequest-trailers.js
+++ b/test/parallel/test-http2-compat-serverrequest-trailers.js
@@ -19,7 +19,7 @@ server.listen(0, common.mustCall(function() {
server.once('request', common.mustCall(function(request, response) {
let data = '';
request.setEncoding('utf8');
- request.on('data', common.mustCall((chunk) => data += chunk));
+ request.on('data', common.mustCallAtLeast((chunk) => data += chunk));
request.on('end', common.mustCall(() => {
const trailers = request.trailers;
for (const [name, value] of Object.entries(expectedTrailers)) {
diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js
index 4c7cf3d85c..7461176c5f 100644
--- a/test/parallel/test-http2-getpackedsettings.js
+++ b/test/parallel/test-http2-getpackedsettings.js
@@ -104,7 +104,8 @@ assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false }));
}, common.expectsError({
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
- message: 'The "buf" argument must be one of type Buffer or Uint8Array'
+ message:
+ 'The "buf" argument must be one of type Buffer, TypedArray, or DataView'
}));
});
diff --git a/test/parallel/test-http2-info-headers-errors.js b/test/parallel/test-http2-info-headers-errors.js
index 5e1c2d1fad..b671bece4f 100644
--- a/test/parallel/test-http2-info-headers-errors.js
+++ b/test/parallel/test-http2-info-headers-errors.js
@@ -6,29 +6,15 @@ if (!common.hasCrypto)
const http2 = require('http2');
const {
constants,
- Http2Session,
+ Http2Stream,
nghttp2ErrorString
} = process.binding('http2');
// tests error handling within additionalHeaders
-// - NGHTTP2_ERR_NOMEM (should emit session error)
// - every other NGHTTP2 error from binding (should emit stream error)
-const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM'
-];
-
-const specificTests = [
- {
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- },
- type: 'session'
- }
-];
+const specificTestKeys = [];
+const specificTests = [];
const genericTests = Object.getOwnPropertyNames(constants)
.filter((key) => (
@@ -50,7 +36,7 @@ const tests = specificTests.concat(genericTests);
let currentError;
// mock sendHeaders because we only care about testing error handling
-Http2Session.prototype.sendHeaders = () => currentError.ngError;
+Http2Stream.prototype.info = () => currentError.ngError;
const server = http2.createServer();
server.on('stream', common.mustCall((stream, headers) => {
diff --git a/test/parallel/test-http2-invalidargtypes-errors.js b/test/parallel/test-http2-invalidargtypes-errors.js
index 93d161557c..3471e46fdf 100644
--- a/test/parallel/test-http2-invalidargtypes-errors.js
+++ b/test/parallel/test-http2-invalidargtypes-errors.js
@@ -16,15 +16,7 @@ server.on(
message: `The "${param}" argument must be of type ${type}`
});
common.expectsError(
- () => stream.session.priority(undefined, {}),
- invalidArgTypeError('stream', 'Http2Stream')
- );
- common.expectsError(
- () => stream.session.rstStream(undefined),
- invalidArgTypeError('stream', 'Http2Stream')
- );
- common.expectsError(
- () => stream.session.rstStream(stream, 'string'),
+ () => stream.rstStream('string'),
invalidArgTypeError('code', 'number')
);
stream.session.destroy();
diff --git a/test/parallel/test-http2-no-more-streams.js b/test/parallel/test-http2-no-more-streams.js
new file mode 100644
index 0000000000..6f4169756c
--- /dev/null
+++ b/test/parallel/test-http2-no-more-streams.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const http2 = require('http2');
+const Countdown = require('../common/countdown');
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respond();
+ stream.end('ok');
+});
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const nextID = 2 ** 31 - 1;
+
+ client.on('connect', () => {
+ client.setNextStreamID(nextID);
+
+ assert.strictEqual(client.state.nextStreamID, nextID);
+
+ const countdown = new Countdown(2, common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+
+ {
+ // This one will be ok
+ const req = client.request();
+ assert.strictEqual(req.id, nextID);
+
+ req.on('error', common.mustNotCall());
+ req.resume();
+ req.on('end', () => countdown.dec());
+ }
+
+ {
+ // This one will error because there are no more stream IDs available
+ const req = client.request();
+ req.on('error', common.expectsError({
+ code: 'ERR_HTTP2_OUT_OF_STREAMS',
+ type: Error,
+ message:
+ 'No stream ID is available because maximum stream ID has been reached'
+ }));
+ req.on('error', () => countdown.dec());
+ }
+ });
+}));
diff --git a/test/parallel/test-http2-ping.js b/test/parallel/test-http2-ping.js
new file mode 100644
index 0000000000..4892d67b4d
--- /dev/null
+++ b/test/parallel/test-http2-ping.js
@@ -0,0 +1,87 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const async_hooks = require('async_hooks');
+const assert = require('assert');
+const http2 = require('http2');
+
+const pings = new Set();
+const events = [0, 0, 0, 0];
+
+const hook = async_hooks.createHook({
+ init(id, type, trigger, resource) {
+ if (type === 'HTTP2PING') {
+ pings.add(id);
+ events[0]++;
+ }
+ },
+ before(id) {
+ if (pings.has(id)) {
+ events[1]++;
+ }
+ },
+ after(id) {
+ if (pings.has(id)) {
+ events[2]++;
+ }
+ },
+ destroy(id) {
+ if (pings.has(id)) {
+ events[3]++;
+ }
+ }
+});
+hook.enable();
+
+process.on('exit', () => {
+ assert.deepStrictEqual(events, [4, 4, 4, 4]);
+});
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream) => {
+ assert(stream.session.ping(common.mustCall((err, duration, ret) => {
+ assert.strictEqual(err, null);
+ assert.strictEqual(typeof duration, 'number');
+ assert.strictEqual(ret.length, 8);
+ stream.end('ok');
+ })));
+ stream.respond();
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`,
+ { maxOutstandingPings: 2 });
+ client.on('connect', common.mustCall(() => {
+ {
+ const payload = Buffer.from('abcdefgh');
+ assert(client.ping(payload, common.mustCall((err, duration, ret) => {
+ assert.strictEqual(err, null);
+ assert.strictEqual(typeof duration, 'number');
+ assert.deepStrictEqual(payload, ret);
+ })));
+ }
+ {
+ const payload = Buffer.from('abcdefgi');
+ assert(client.ping(payload, common.mustCall((err, duration, ret) => {
+ assert.strictEqual(err, null);
+ assert.strictEqual(typeof duration, 'number');
+ assert.deepStrictEqual(payload, ret);
+ })));
+ }
+ // Only max 2 pings at a time based on the maxOutstandingPings option
+ assert(!client.ping(common.expectsError({
+ code: 'ERR_HTTP2_PING_CANCEL',
+ type: Error,
+ message: 'HTTP2 ping cancelled'
+ })));
+ const req = client.request();
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ client.destroy();
+ server.close();
+ }));
+ }));
+}));
diff --git a/test/parallel/test-http2-pipe.js b/test/parallel/test-http2-pipe.js
index 819fab5154..8b446f4f88 100644
--- a/test/parallel/test-http2-pipe.js
+++ b/test/parallel/test-http2-pipe.js
@@ -8,6 +8,7 @@ const assert = require('assert');
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
+const Countdown = require('../common/countdown');
// piping should work as expected with createWriteStream
@@ -31,19 +32,16 @@ server.listen(0, common.mustCall(() => {
const port = server.address().port;
const client = http2.connect(`http://localhost:${port}`);
- let remaining = 2;
- function maybeClose() {
- if (--remaining === 0) {
- server.close();
- client.destroy();
- }
- }
+ const countdown = new Countdown(2, common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
const req = client.request({ ':method': 'POST' });
req.on('response', common.mustCall());
req.resume();
- req.on('end', common.mustCall(maybeClose));
+ req.on('end', common.mustCall(() => countdown.dec()));
const str = fs.createReadStream(loc);
- str.on('end', common.mustCall(maybeClose));
+ str.on('end', common.mustCall(() => countdown.dec()));
str.pipe(req);
}));
diff --git a/test/parallel/test-http2-priority-errors.js b/test/parallel/test-http2-priority-errors.js
deleted file mode 100644
index d29d2f72fa..0000000000
--- a/test/parallel/test-http2-priority-errors.js
+++ /dev/null
@@ -1,109 +0,0 @@
-'use strict';
-
-const common = require('../common');
-if (!common.hasCrypto)
- common.skip('missing crypto');
-const http2 = require('http2');
-const {
- constants,
- Http2Session,
- nghttp2ErrorString
-} = process.binding('http2');
-
-// tests error handling within priority
-// - NGHTTP2_ERR_NOMEM (should emit session error)
-// - every other NGHTTP2 error from binding (should emit stream error)
-
-const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM'
-];
-
-const specificTests = [
- {
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- },
- type: 'session'
- }
-];
-
-const genericTests = Object.getOwnPropertyNames(constants)
- .filter((key) => (
- key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
- ))
- .map((key) => ({
- ngError: constants[key],
- error: {
- code: 'ERR_HTTP2_ERROR',
- type: Error,
- message: nghttp2ErrorString(constants[key])
- },
- type: 'stream'
- }));
-
-
-const tests = specificTests.concat(genericTests);
-
-let currentError;
-
-// mock submitPriority because we only care about testing error handling
-Http2Session.prototype.submitPriority = () => currentError.ngError;
-
-const server = http2.createServer();
-server.on('stream', common.mustCall((stream, headers) => {
- const errorMustCall = common.expectsError(currentError.error);
- const errorMustNotCall = common.mustNotCall(
- `${currentError.error.code} should emit on ${currentError.type}`
- );
-
- if (currentError.type === 'stream') {
- stream.session.on('error', errorMustNotCall);
- stream.on('error', errorMustCall);
- stream.on('error', common.mustCall(() => {
- stream.respond();
- stream.end();
- }));
- } else {
- stream.session.once('error', errorMustCall);
- stream.on('error', errorMustNotCall);
- }
-
- stream.priority({
- parent: 0,
- weight: 1,
- exclusive: false
- });
-}, tests.length));
-
-server.listen(0, common.mustCall(() => runTest(tests.shift())));
-
-function runTest(test) {
- const port = server.address().port;
- const url = `http://localhost:${port}`;
- const headers = {
- ':path': '/',
- ':method': 'POST',
- ':scheme': 'http',
- ':authority': `localhost:${port}`
- };
-
- const client = http2.connect(url);
- const req = client.request(headers);
-
- currentError = test;
- req.resume();
- req.end();
-
- req.on('end', common.mustCall(() => {
- client.destroy();
-
- if (!tests.length) {
- server.close();
- } else {
- runTest(tests.shift());
- }
- }));
-}
diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js
index 4e2c39178e..dcc05357fa 100644
--- a/test/parallel/test-http2-respond-errors.js
+++ b/test/parallel/test-http2-respond-errors.js
@@ -6,29 +6,16 @@ if (!common.hasCrypto)
const http2 = require('http2');
const {
constants,
- Http2Session,
+ Http2Stream,
nghttp2ErrorString
} = process.binding('http2');
// tests error handling within respond
-// - NGHTTP2_ERR_NOMEM (should emit session error)
// - every other NGHTTP2 error from binding (should emit stream error)
-const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM'
-];
+const specificTestKeys = [];
-const specificTests = [
- {
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- },
- type: 'session'
- }
-];
+const specificTests = [];
const genericTests = Object.getOwnPropertyNames(constants)
.filter((key) => (
@@ -50,7 +37,7 @@ const tests = specificTests.concat(genericTests);
let currentError;
// mock submitResponse because we only care about testing error handling
-Http2Session.prototype.submitResponse = () => currentError.ngError;
+Http2Stream.prototype.respond = () => currentError.ngError;
const server = http2.createServer();
server.on('stream', common.mustCall((stream, headers) => {
diff --git a/test/parallel/test-http2-respond-with-fd-errors.js b/test/parallel/test-http2-respond-with-fd-errors.js
index 920c3eb908..c8ecfcf5f3 100644
--- a/test/parallel/test-http2-respond-with-fd-errors.js
+++ b/test/parallel/test-http2-respond-with-fd-errors.js
@@ -11,32 +11,18 @@ const http2 = require('http2');
const {
constants,
- Http2Session,
+ Http2Stream,
nghttp2ErrorString
} = process.binding('http2');
// tests error handling within processRespondWithFD
// (called by respondWithFD & respondWithFile)
-// - NGHTTP2_ERR_NOMEM (should emit session error)
// - every other NGHTTP2 error from binding (should emit stream error)
const fname = fixtures.path('elipses.txt');
-const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM'
-];
-
-const specificTests = [
- {
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- },
- type: 'session'
- }
-];
+const specificTestKeys = [];
+const specificTests = [];
const genericTests = Object.getOwnPropertyNames(constants)
.filter((key) => (
@@ -57,8 +43,8 @@ const tests = specificTests.concat(genericTests);
let currentError;
-// mock submitFile because we only care about testing error handling
-Http2Session.prototype.submitFile = () => currentError.ngError;
+// mock respondFD because we only care about testing error handling
+Http2Stream.prototype.respondFD = () => currentError.ngError;
const server = http2.createServer();
server.on('stream', common.mustCall((stream, headers) => {
diff --git a/test/parallel/test-http2-rststream-errors.js b/test/parallel/test-http2-rststream-errors.js
index 58d4440f2e..f53956ce99 100644
--- a/test/parallel/test-http2-rststream-errors.js
+++ b/test/parallel/test-http2-rststream-errors.js
@@ -6,29 +6,15 @@ if (!common.hasCrypto)
const http2 = require('http2');
const {
constants,
- Http2Session,
+ Http2Stream,
nghttp2ErrorString
} = process.binding('http2');
// tests error handling within rstStream
-// - NGHTTP2_ERR_NOMEM (should emit session error)
// - every other NGHTTP2 error from binding (should emit stream error)
-const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM'
-];
-
-const specificTests = [
- {
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- },
- type: 'session'
- }
-];
+const specificTestKeys = [];
+const specificTests = [];
const genericTests = Object.getOwnPropertyNames(constants)
.filter((key) => (
@@ -50,7 +36,7 @@ const tests = specificTests.concat(genericTests);
let currentError;
// mock submitRstStream because we only care about testing error handling
-Http2Session.prototype.submitRstStream = () => currentError.ngError;
+Http2Stream.prototype.rstStream = () => currentError.ngError;
const server = http2.createServer();
server.on('stream', common.mustCall((stream, headers) => {
diff --git a/test/parallel/test-http2-server-http1-client.js b/test/parallel/test-http2-server-http1-client.js
new file mode 100644
index 0000000000..ef3a79c0fd
--- /dev/null
+++ b/test/parallel/test-http2-server-http1-client.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const common = require('../common');
+
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const http = require('http');
+const http2 = require('http2');
+
+const server = http2.createServer();
+server.on('stream', common.mustNotCall());
+server.on('session', common.mustCall((session) => {
+ session.on('close', common.mustCall());
+}));
+
+server.listen(0, common.mustCall(() => {
+ const req = http.get(`http://localhost:${server.address().port}`);
+ req.on('error', (error) => {
+ server.close();
+ });
+}));
diff --git a/test/parallel/test-http2-server-push-stream-errors.js b/test/parallel/test-http2-server-push-stream-errors.js
index 777b20eb3f..56e329dcff 100644
--- a/test/parallel/test-http2-server-push-stream-errors.js
+++ b/test/parallel/test-http2-server-push-stream-errors.js
@@ -6,33 +6,22 @@ if (!common.hasCrypto)
const http2 = require('http2');
const {
constants,
- Http2Session,
+ Http2Stream,
nghttp2ErrorString
} = process.binding('http2');
// tests error handling within pushStream
-// - NGHTTP2_ERR_NOMEM (should emit session error)
// - NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE (should emit session error)
// - NGHTTP2_ERR_STREAM_CLOSED (should emit stream error)
// - every other NGHTTP2 error from binding (should emit stream error)
const specificTestKeys = [
- 'NGHTTP2_ERR_NOMEM',
'NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE',
'NGHTTP2_ERR_STREAM_CLOSED'
];
const specificTests = [
{
- ngError: constants.NGHTTP2_ERR_NOMEM,
- error: {
- code: 'ERR_OUTOFMEMORY',
- type: Error,
- message: 'Out of memory'
- },
- type: 'session'
- },
- {
ngError: constants.NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
error: {
code: 'ERR_HTTP2_OUT_OF_STREAMS',
@@ -40,7 +29,7 @@ const specificTests = [
message: 'No stream ID is available because ' +
'maximum stream ID has been reached'
},
- type: 'session'
+ type: 'stream'
},
{
ngError: constants.NGHTTP2_ERR_STREAM_CLOSED,
@@ -73,7 +62,7 @@ const tests = specificTests.concat(genericTests);
let currentError;
// mock submitPushPromise because we only care about testing error handling
-Http2Session.prototype.submitPushPromise = () => currentError.ngError;
+Http2Stream.prototype.pushPromise = () => currentError.ngError;
const server = http2.createServer();
server.on('stream', common.mustCall((stream, headers) => {
diff --git a/test/parallel/test-http2-server-stream-session-destroy.js b/test/parallel/test-http2-server-stream-session-destroy.js
index f2cc4a1f77..24d064a448 100644
--- a/test/parallel/test-http2-server-stream-session-destroy.js
+++ b/test/parallel/test-http2-server-stream-session-destroy.js
@@ -15,7 +15,7 @@ server.on(
// Test that stream.state getter returns an empty object
// when the stream session has been destroyed
- assert.deepStrictEqual(Object.create(null), stream.state);
+ assert.deepStrictEqual({}, stream.state);
// Test that ERR_HTTP2_INVALID_STREAM is thrown while calling
// stream operations after the stream session has been destroyed
@@ -31,7 +31,6 @@ server.on(
invalidStreamError
);
common.expectsError(() => stream.respond(), invalidStreamError);
- common.expectsError(() => stream.rstStream(), invalidStreamError);
common.expectsError(() => stream.write('data'), invalidStreamError);
// Test that ERR_HTTP2_INVALID_SESSION is thrown while calling
@@ -41,17 +40,14 @@ server.on(
code: 'ERR_HTTP2_INVALID_SESSION',
message: 'The session has been destroyed'
};
- common.expectsError(() => stream.session.priority(), invalidSessionError);
common.expectsError(() => stream.session.settings(), invalidSessionError);
common.expectsError(() => stream.session.shutdown(), invalidSessionError);
// Wait for setImmediate call from destroy() to complete
// so that state.destroyed is set to true
setImmediate((session) => {
- common.expectsError(() => session.priority(), invalidSessionError);
common.expectsError(() => session.settings(), invalidSessionError);
common.expectsError(() => session.shutdown(), invalidSessionError);
- common.expectsError(() => session.rstStream(), invalidSessionError);
}, stream.session);
})
);
diff --git a/test/parallel/test-http2-shutdown-errors.js b/test/parallel/test-http2-shutdown-errors.js
index 99ae791767..30bdb7a986 100644
--- a/test/parallel/test-http2-shutdown-errors.js
+++ b/test/parallel/test-http2-shutdown-errors.js
@@ -29,7 +29,7 @@ const tests = Object.getOwnPropertyNames(constants)
let currentError;
// mock submitGoaway because we only care about testing error handling
-Http2Session.prototype.submitGoaway = () => currentError.ngError;
+Http2Session.prototype.goaway = () => currentError.ngError;
const server = http2.createServer();
server.on('stream', common.mustCall((stream, headers) => {
diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js
index 83c97c06b0..4388d55682 100644
--- a/test/parallel/test-http2-util-update-options-buffer.js
+++ b/test/parallel/test-http2-util-update-options-buffer.js
@@ -16,7 +16,8 @@ const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2;
const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3;
const IDX_OPTIONS_PADDING_STRATEGY = 4;
const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
-const IDX_OPTIONS_FLAGS = 6;
+const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
+const IDX_OPTIONS_FLAGS = 7;
{
updateOptionsBuffer({
@@ -25,7 +26,8 @@ const IDX_OPTIONS_FLAGS = 6;
maxSendHeaderBlockLength: 3,
peerMaxConcurrentStreams: 4,
paddingStrategy: 5,
- maxHeaderListPairs: 6
+ maxHeaderListPairs: 6,
+ maxOutstandingPings: 7
});
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -34,6 +36,7 @@ const IDX_OPTIONS_FLAGS = 6;
strictEqual(optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS], 4);
strictEqual(optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY], 5);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6);
+ strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7);
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
@@ -43,10 +46,12 @@ const IDX_OPTIONS_FLAGS = 6;
ok(flags & (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS));
ok(flags & (1 << IDX_OPTIONS_PADDING_STRATEGY));
ok(flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS));
+ ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS));
}
{
optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] = 0;
+ optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] = 0;
updateOptionsBuffer({
maxDeflateDynamicTableSize: 1,
@@ -58,17 +63,20 @@ const IDX_OPTIONS_FLAGS = 6;
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS], 2);
- strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH], 0);
strictEqual(optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS], 4);
strictEqual(optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY], 5);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6);
+ strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH], 0);
+ strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 0);
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
ok(flags & (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE));
ok(flags & (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS));
- ok(!(flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)));
ok(flags & (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS));
ok(flags & (1 << IDX_OPTIONS_PADDING_STRATEGY));
ok(flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS));
+
+ ok(!(flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)));
+ ok(!(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)));
}
diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js
index 1ee5077160..3c82421706 100644
--- a/test/sequential/test-async-wrap-getasyncid.js
+++ b/test/sequential/test-async-wrap-getasyncid.js
@@ -23,7 +23,8 @@ const fixtures = require('../common/fixtures');
// TODO(jasnell): Test for these
delete providers.HTTP2SESSION;
- delete providers.HTTP2SESSIONSHUTDOWNWRAP;
+ delete providers.HTTP2STREAM;
+ delete providers.HTTP2PING;
const obj_keys = Object.keys(providers);
if (obj_keys.length > 0)