http2: refactor how trailers are done

Rather than an option, introduce a method and an event...

```js
server.on('stream', (stream) => {
  stream.respond(undefined, { waitForTrailers: true });
  stream.on('wantTrailers', () => {
    stream.sendTrailers({ abc: 'xyz'});
  });
  stream.end('hello world');
});
```

This is a breaking change in the API such that the prior
`options.getTrailers` is no longer supported at all.
Ordinarily this would be semver-major and require a
deprecation but the http2 stuff is still experimental.

PR-URL: https://github.com/nodejs/node/pull/19959
Reviewed-By: Yuta Hiroto <hello@hiroppy.me>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
James M Snell 2018-04-11 16:11:35 -07:00
parent 2e76b175ed
commit 237aa7e9ae
17 changed files with 329 additions and 285 deletions

View File

@ -1017,6 +1017,19 @@ When setting the priority for an HTTP/2 stream, the stream may be marked as
a dependency for a parent stream. This error code is used when an attempt is
made to mark a stream and dependent of itself.
<a id="ERR_HTTP2_TRAILERS_ALREADY_SENT"></a>
### ERR_HTTP2_TRAILERS_ALREADY_SENT
Trailing headers have already been sent on the `Http2Stream`.
<a id="ERR_HTTP2_TRAILERS_NOT_READY"></a>
### ERR_HTTP2_TRAILERS_NOT_READY
The `http2stream.sendTrailers()` method cannot be called until after the
`'wantTrailers'` event is emitted on an `Http2Stream` object. The
`'wantTrailers'` event will only be emitted if the `waitForTrailers` option
is set for the `Http2Stream`.
<a id="ERR_HTTP2_UNSUPPORTED_PROTOCOL"></a>
### ERR_HTTP2_UNSUPPORTED_PROTOCOL

View File

@ -176,13 +176,13 @@ immediately following the `'frameError'` event.
added: v8.4.0
-->
The `'goaway'` event is emitted when a GOAWAY frame is received. When invoked,
The `'goaway'` event is emitted when a `GOAWAY` frame is received. When invoked,
the handler function will receive three arguments:
* `errorCode` {number} The HTTP/2 error code specified in the GOAWAY frame.
* `errorCode` {number} The HTTP/2 error code specified in the `GOAWAY` frame.
* `lastStreamID` {number} The ID of the last stream the remote peer successfully
processed (or `0` if no ID is specified).
* `opaqueData` {Buffer} If additional opaque data was included in the GOAWAY
* `opaqueData` {Buffer} If additional opaque data was included in the `GOAWAY`
frame, a `Buffer` instance will be passed containing that data.
The `Http2Session` instance will be shut down automatically when the `'goaway'`
@ -193,7 +193,7 @@ event is emitted.
added: v8.4.0
-->
The `'localSettings'` event is emitted when an acknowledgment SETTINGS frame
The `'localSettings'` event is emitted when an acknowledgment `SETTINGS` frame
has been received. When invoked, the handler function will receive a copy of
the local settings.
@ -213,7 +213,7 @@ session.on('localSettings', (settings) => {
added: v8.4.0
-->
The `'remoteSettings'` event is emitted when a new SETTINGS frame is received
The `'remoteSettings'` event is emitted when a new `SETTINGS` frame is received
from the connected peer. When invoked, the handler function will receive a copy
of the remote settings.
@ -383,7 +383,7 @@ added: v9.4.0
* `code` {number} An HTTP/2 error code
* `lastStreamID` {number} The numeric ID of the last processed `Http2Stream`
* `opaqueData` {Buffer|TypedArray|DataView} A `TypedArray` or `DataView`
instance containing additional data to be carried within the GOAWAY frame.
instance containing additional data to be carried within the `GOAWAY` frame.
Transmits a `GOAWAY` frame to the connected peer *without* shutting down the
`Http2Session`.
@ -417,7 +417,7 @@ added: v8.4.0
* {boolean}
Indicates whether or not the `Http2Session` is currently waiting for an
acknowledgment for a sent SETTINGS frame. Will be `true` after calling the
acknowledgment 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.
@ -551,9 +551,9 @@ Once called, the `http2session.pendingSettingsAck` property will be `true`
while the session is waiting for the remote peer to acknowledge the new
settings.
The new settings will not become effective until the SETTINGS acknowledgment is
received and the `'localSettings'` event is emitted. It is possible to send
multiple SETTINGS frames while acknowledgment is still pending.
The new settings will not become effective until the `SETTINGS` acknowledgment
is received and the `'localSettings'` event is emitted. It is possible to send
multiple `SETTINGS` frames while acknowledgment is still pending.
#### http2session.type
<!-- YAML
@ -695,8 +695,8 @@ added: v8.4.0
* `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).
* `getTrailers` {Function} Callback function invoked to collect trailer
headers.
* `waitForTrailers` {boolean} When `true`, the `Http2Stream` will emit the
`'wantTrailers'` event after the final `DATA` frame has been sent.
* Returns: {ClientHttp2Stream}
@ -723,14 +723,15 @@ req.on('response', (headers) => {
});
```
When set, the `options.getTrailers()` function is called immediately after
queuing the last chunk of payload data to be sent. The callback is passed a
single object (with a `null` prototype) that the listener may use to specify
the trailing header fields to send to the peer.
When the `options.waitForTrailers` option is set, the `'wantTrailers'` event
is emitted immediately after queuing the last chunk of payload data to be sent.
The `http2stream.sendTrailers()` method can then be called to send trailing
headers to the peer.
The HTTP/1 specification forbids trailers from containing HTTP/2 pseudo-header
fields (e.g. `':method'`, `':path'`, etc). An `'error'` event will be emitted
if the `getTrailers` callback attempts to set such header fields.
It is important to note that when `options.waitForTrailers` is set, the
`Http2Stream` will *not* automatically close when the final `DATA` frame is
transmitted. User code *must* call either `http2stream.sendTrailers()` or
`http2stream.close()` to close the `Http2Stream`.
The `:method` and `:path` pseudo-headers are not specified within `headers`,
they respectively default to:
@ -875,6 +876,16 @@ stream.on('trailers', (headers, flags) => {
});
```
#### Event: 'wantTrailers'
<!-- YAML
added: REPLACEME
-->
The `'wantTrailers'` event is emitted when the `Http2Stream` has queued the
final `DATA` frame to be sent on a frame and the `Http2Stream` is ready to send
trailing headers. When initiating a request or response, the `waitForTrailers`
option must be set for this event to be emitted.
#### http2stream.aborted
<!-- YAML
added: v8.4.0
@ -1037,6 +1048,35 @@ Provides miscellaneous information about the current state of the
A current state of this `Http2Stream`.
#### http2stream.sendTrailers(headers)
<!-- YAML
added: REPLACEME
-->
* `headers` {HTTP/2 Headers Object}
Sends a trailing `HEADERS` frame to the connected HTTP/2 peer. This method
will cause the `Http2Stream` to be immediately closed and must only be
called after the `'wantTrailers'` event has been emitted. When sending a
request or sending a response, the `options.waitForTrailers` option must be set
in order to keep the `Http2Stream` open after the final `DATA` frame so that
trailers can be sent.
```js
const http2 = require('http2');
const server = http2.createServer();
server.on('stream', (stream) => {
stream.respond(undefined, { waitForTrailers: true });
stream.on('wantTrailers', () => {
stream.sendTrailers({ xyz: 'abc' });
});
stream.end('Hello World');
});
```
The HTTP/1 specification forbids trailers from containing HTTP/2 pseudo-header
fields (e.g. `':method'`, `':path'`, etc).
### Class: ClientHttp2Stream
<!-- YAML
added: v8.4.0
@ -1201,8 +1241,8 @@ added: v8.4.0
* `options` {Object}
* `endStream` {boolean} Set to `true` to indicate that the response will not
include payload data.
* `getTrailers` {Function} Callback function invoked to collect trailer
headers.
* `waitForTrailers` {boolean} When `true`, the `Http2Stream` will emit the
`'wantTrailers'` event after the final `DATA` frame has been sent.
```js
const http2 = require('http2');
@ -1213,28 +1253,28 @@ server.on('stream', (stream) => {
});
```
When set, the `options.getTrailers()` function is called immediately after
queuing the last chunk of payload data to be sent. The callback is passed a
single object (with a `null` prototype) that the listener may use to specify
the trailing header fields to send to the peer.
When the `options.waitForTrailers` option is set, the `'wantTrailers'` event
will be emitted immediately after queuing the last chunk of payload data to be
sent. The `http2stream.sendTrailers()` method can then be used to sent trailing
header fields to the peer.
It is important to note that when `options.waitForTrailers` is set, the
`Http2Stream` will *not* automatically close when the final `DATA` frame is
transmitted. User code *must* call either `http2stream.sendTrailers()` or
`http2stream.close()` to close the `Http2Stream`.
```js
const http2 = require('http2');
const server = http2.createServer();
server.on('stream', (stream) => {
stream.respond({ ':status': 200 }, {
getTrailers(trailers) {
trailers.ABC = 'some value to send';
}
stream.respond({ ':status': 200 }, { waitForTrailers: true });
stream.on('wantTrailers', () => {
stream.sendTrailers({ ABC: 'some value to send' });
});
stream.end('some data');
});
```
The HTTP/1 specification forbids trailers from containing HTTP/2 pseudo-header
fields (e.g. `':status'`, `':path'`, etc). An `'error'` event will be emitted
if the `getTrailers` callback attempts to set such header fields.
#### http2stream.respondWithFD(fd[, headers[, options]])
<!-- YAML
added: v8.4.0
@ -1249,8 +1289,8 @@ changes:
* `headers` {HTTP/2 Headers Object}
* `options` {Object}
* `statCheck` {Function}
* `getTrailers` {Function} Callback function invoked to collect trailer
headers.
* `waitForTrailers` {boolean} When `true`, the `Http2Stream` will emit the
`'wantTrailers'` event after the final `DATA` frame has been sent.
* `offset` {number} The offset position at which to begin reading.
* `length` {number} The amount of data from the fd to send.
@ -1297,10 +1337,15 @@ Note that using the same file descriptor concurrently for multiple streams
is not supported and may result in data loss. Re-using a file descriptor
after a stream has finished is supported.
When set, the `options.getTrailers()` function is called immediately after
queuing the last chunk of payload data to be sent. The callback is passed a
single object (with a `null` prototype) that the listener may use to specify
the trailing header fields to send to the peer.
When the `options.waitForTrailers` option is set, the `'wantTrailers'` event
will be emitted immediately after queuing the last chunk of payload data to be
sent. The `http2stream.sendTrailers()` method can then be used to sent trailing
header fields to the peer.
It is important to note that when `options.waitForTrailers` is set, the
`Http2Stream` will *not* automatically close when the final `DATA` frame is
transmitted. User code *must* call either `http2stream.sendTrailers()` or
`http2stream.close()` to close the `Http2Stream`.
```js
const http2 = require('http2');
@ -1316,20 +1361,15 @@ server.on('stream', (stream) => {
'last-modified': stat.mtime.toUTCString(),
'content-type': 'text/plain'
};
stream.respondWithFD(fd, headers, {
getTrailers(trailers) {
trailers.ABC = 'some value to send';
}
stream.respondWithFD(fd, headers, { waitForTrailers: true });
stream.on('wantTrailers', () => {
stream.sendTrailers({ ABC: 'some value to send' });
});
stream.on('close', () => fs.closeSync(fd));
});
```
The HTTP/1 specification forbids trailers from containing HTTP/2 pseudo-header
fields (e.g. `':status'`, `':path'`, etc). An `'error'` event will be emitted
if the `getTrailers` callback attempts to set such header fields.
#### http2stream.respondWithFile(path[, headers[, options]])
<!-- YAML
added: v8.4.0
@ -1346,8 +1386,8 @@ changes:
* `statCheck` {Function}
* `onError` {Function} Callback function invoked in the case of an
Error before send.
* `getTrailers` {Function} Callback function invoked to collect trailer
headers.
* `waitForTrailers` {boolean} When `true`, the `Http2Stream` will emit the
`'wantTrailers'` event after the final `DATA` frame has been sent.
* `offset` {number} The offset position at which to begin reading.
* `length` {number} The amount of data from the fd to send.
@ -1421,28 +1461,29 @@ The `options.onError` function may also be used to handle all the errors
that could happen before the delivery of the file is initiated. The
default behavior is to destroy the stream.
When set, the `options.getTrailers()` function is called immediately after
queuing the last chunk of payload data to be sent. The callback is passed a
single object (with a `null` prototype) that the listener may use to specify
the trailing header fields to send to the peer.
When the `options.waitForTrailers` option is set, the `'wantTrailers'` event
will be emitted immediately after queuing the last chunk of payload data to be
sent. The `http2stream.sendTrilers()` method can then be used to sent trailing
header fields to the peer.
It is important to note that when `options.waitForTrailers` is set, the
`Http2Stream` will *not* automatically close when the final `DATA` frame is
transmitted. User code *must* call either `http2stream.sendTrailers()` or
`http2stream.close()` to close the `Http2Stream`.
```js
const http2 = require('http2');
const server = http2.createServer();
server.on('stream', (stream) => {
function getTrailers(trailers) {
trailers.ABC = 'some value to send';
}
stream.respondWithFile('/some/file',
{ 'content-type': 'text/plain' },
{ getTrailers });
{ waitForTrailers: true });
stream.on('wantTrailers', () => {
stream.sendTrailers({ ABC: 'some value to send' });
});
});
```
The HTTP/1 specification forbids trailers from containing HTTP/2 pseudo-header
fields (e.g. `':status'`, `':path'`, etc). An `'error'` event will be emitted
if the `getTrailers` callback attempts to set such header fields.
### Class: Http2Server
<!-- YAML
added: v8.4.0
@ -1709,7 +1750,7 @@ changes:
limit to be exceeded, but new `Http2Stream` instances will be rejected
while this limit is exceeded. The current number of `Http2Stream` sessions,
the current memory use of the header compression tables, current data
queued to be sent, and unacknowledged PING and SETTINGS frames are all
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
counted towards the current limit. **Default:** `10`.
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
The minimum value is `4`. **Default:** `128`.
@ -1720,7 +1761,7 @@ changes:
exceed this limit will result in a `'frameError'` event being emitted
and the stream being closed and destroyed.
* `paddingStrategy` {number} Identifies the strategy used for determining the
amount of padding to use for HEADERS and DATA frames. **Default:**
amount of padding to use for `HEADERS` and `DATA` frames. **Default:**
`http2.constants.PADDING_STRATEGY_NONE`. Value may be one of:
* `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is
to be applied.
@ -1738,7 +1779,7 @@ changes:
calculated amount needed to ensure alignment, the maximum will be used
and the total frame length will *not* necessarily be aligned at 8 bytes.
* `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
streams for the remote peer as if a SETTINGS frame had been received. Will
streams for the remote peer as if a `SETTINGS` frame had been received. Will
be overridden if the remote peer sets its own value for
`maxConcurrentStreams`. **Default:** `100`.
* `selectPadding` {Function} When `options.paddingStrategy` is equal to
@ -1819,7 +1860,7 @@ changes:
limit to be exceeded, but new `Http2Stream` instances will be rejected
while this limit is exceeded. The current number of `Http2Stream` sessions,
the current memory use of the header compression tables, current data
queued to be sent, and unacknowledged PING and SETTINGS frames are all
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
counted towards the current limit. **Default:** `10`.
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
The minimum value is `4`. **Default:** `128`.
@ -1830,7 +1871,7 @@ changes:
exceed this limit will result in a `'frameError'` event being emitted
and the stream being closed and destroyed.
* `paddingStrategy` {number} Identifies the strategy used for determining the
amount of padding to use for HEADERS and DATA frames. **Default:**
amount of padding to use for `HEADERS` and `DATA` frames. **Default:**
`http2.constants.PADDING_STRATEGY_NONE`. Value may be one of:
* `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is
to be applied.
@ -1848,7 +1889,7 @@ changes:
calculated amount needed to ensure alignment, the maximum will be used
and the total frame length will *not* necessarily be aligned at 8 bytes.
* `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
streams for the remote peer as if a SETTINGS frame had been received. Will
streams for the remote peer as if a `SETTINGS` frame had been received. Will
be overridden if the remote peer sets its own value for
`maxConcurrentStreams`. **Default:** `100`.
* `selectPadding` {Function} When `options.paddingStrategy` is equal to
@ -1911,7 +1952,7 @@ changes:
limit to be exceeded, but new `Http2Stream` instances will be rejected
while this limit is exceeded. The current number of `Http2Stream` sessions,
the current memory use of the header compression tables, current data
queued to be sent, and unacknowledged PING and SETTINGS frames are all
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
counted towards the current limit. **Default:** `10`.
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
The minimum value is `1`. **Default:** `128`.
@ -1926,7 +1967,7 @@ changes:
exceed this limit will result in a `'frameError'` event being emitted
and the stream being closed and destroyed.
* `paddingStrategy` {number} Identifies the strategy used for determining the
amount of padding to use for HEADERS and DATA frames. **Default:**
amount of padding to use for `HEADERS` and `DATA` frames. **Default:**
`http2.constants.PADDING_STRATEGY_NONE`. Value may be one of:
* `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is
to be applied.
@ -1944,7 +1985,7 @@ changes:
calculated amount needed to ensure alignment, the maximum will be used
and the total frame length will *not* necessarily be aligned at 8 bytes.
* `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
streams for the remote peer as if a SETTINGS frame had been received. Will
streams for the remote peer as if a `SETTINGS` frame had been received. Will
be overridden if the remote peer sets its own value for
`maxConcurrentStreams`. **Default:** `100`.
* `selectPadding` {Function} When `options.paddingStrategy` is equal to
@ -2115,7 +2156,7 @@ All additional properties on the settings object are ignored.
When `options.paddingStrategy` is equal to
`http2.constants.PADDING_STRATEGY_CALLBACK`, the HTTP/2 implementation will
consult the `options.selectPadding` callback function, if provided, to determine
the specific amount of padding to use per HEADERS and DATA frame.
the specific amount of padding to use per `HEADERS` and `DATA` frame.
The `options.selectPadding` function receives two numeric arguments,
`frameLen` and `maxFrameLen` and must return a number `N` such that
@ -2131,8 +2172,8 @@ const server = http2.createServer({
});
```
The `options.selectPadding` function is invoked once for *every* HEADERS and
DATA frame. This has a definite noticeable impact on performance.
The `options.selectPadding` function is invoked once for *every* `HEADERS` and
`DATA` frame. This has a definite noticeable impact on performance.
### Error Handling
@ -3093,9 +3134,9 @@ The `name` property of the `PerformanceEntry` will be equal to either
If `name` is equal to `Http2Stream`, the `PerformanceEntry` will contain the
following additional properties:
* `bytesRead` {number} The number of DATA frame bytes received for this
* `bytesRead` {number} The number of `DATA` frame bytes received for this
`Http2Stream`.
* `bytesWritten` {number} The number of DATA frame bytes sent for this
* `bytesWritten` {number} The number of `DATA` frame bytes sent for this
`Http2Stream`.
* `id` {number} The identifier of the associated `Http2Stream`
* `timeToFirstByte` {number} The number of milliseconds elapsed between the

View File

@ -842,6 +842,11 @@ E('ERR_HTTP2_STREAM_CANCEL', 'The pending stream has been canceled', Error);
E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error);
E('ERR_HTTP2_STREAM_SELF_DEPENDENCY',
'A stream cannot depend on itself', Error);
E('ERR_HTTP2_TRAILERS_ALREADY_SENT',
'Trailing headers have already been sent', Error);
E('ERR_HTTP2_TRAILERS_NOT_READY',
'Trailing headers cannot be sent until after the wantTrailers event is ' +
'emitted', Error);
E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', 'protocol "%s" is unsupported.', Error);
E('ERR_HTTP_HEADERS_SENT',
'Cannot %s headers after they are sent to the client', Error);

View File

@ -358,6 +358,10 @@ class Http2ServerRequest extends Readable {
}
}
function onStreamTrailersReady() {
this[kStream].sendTrailers(this[kTrailers]);
}
class Http2ServerResponse extends Stream {
constructor(stream, options) {
super(options);
@ -377,6 +381,7 @@ class Http2ServerResponse extends Stream {
stream.on('drain', onStreamDrain);
stream.on('aborted', onStreamAbortedResponse);
stream.on('close', this[kFinish].bind(this));
stream.on('wantTrailers', onStreamTrailersReady.bind(this));
}
// User land modules such as finalhandler just check truthiness of this
@ -648,7 +653,7 @@ class Http2ServerResponse extends Stream {
headers[HTTP2_HEADER_STATUS] = state.statusCode;
const options = {
endStream: state.ending,
getTrailers: (trailers) => Object.assign(trailers, this[kTrailers])
waitForTrailers: true,
};
this[kStream].respond(headers, options);
}

View File

@ -49,6 +49,8 @@ const {
ERR_HTTP2_STREAM_CANCEL,
ERR_HTTP2_STREAM_ERROR,
ERR_HTTP2_STREAM_SELF_DEPENDENCY,
ERR_HTTP2_TRAILERS_ALREADY_SENT,
ERR_HTTP2_TRAILERS_NOT_READY,
ERR_HTTP2_UNSUPPORTED_PROTOCOL,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_CALLBACK,
@ -295,25 +297,18 @@ function tryClose(fd) {
fs.close(fd, (err) => assert.ifError(err));
}
// Called to determine if there are trailers to be sent at the end of a
// Stream. The 'getTrailers' callback is invoked and passed a holder object.
// The trailers to return are set on that object by the handler. Once the
// 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.
// Called when the Http2Stream has finished sending data and is ready for
// trailers to be sent. This will only be called if the { hasOptions: true }
// option is set.
function onStreamTrailers() {
const stream = this[kOwner];
stream[kState].trailersReady = true;
if (stream.destroyed)
return [];
const trailers = Object.create(null);
stream[kState].getTrailers.call(stream, trailers);
const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
if (!Array.isArray(headersList)) {
stream.destroy(headersList);
return [];
return;
if (!stream.emit('wantTrailers')) {
// There are no listeners, send empty trailing HEADERS frame and close.
stream.sendTrailers({});
}
stream[kSentTrailers] = trailers;
return headersList;
}
// Submit an RST-STREAM frame to be sent to the remote peer.
@ -527,10 +522,8 @@ function requestOnConnect(headers, options) {
if (options.endStream)
streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
if (typeof options.getTrailers === 'function') {
if (options.waitForTrailers)
streamOptions |= STREAM_OPTION_GET_TRAILERS;
this[kState].getTrailers = options.getTrailers;
}
// ret will be either the reserved stream ID (if positive)
// or an error code (if negative)
@ -1408,11 +1401,6 @@ class ClientHttp2Session extends Http2Session {
throw new ERR_INVALID_OPT_VALUE('endStream', options.endStream);
}
if (options.getTrailers !== undefined &&
typeof options.getTrailers !== 'function') {
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
}
const headersList = mapToHeaders(headers);
if (!Array.isArray(headersList))
throw headersList;
@ -1504,7 +1492,8 @@ class Http2Stream extends Duplex {
this[kState] = {
flags: STREAM_FLAGS_PENDING,
rstCode: NGHTTP2_NO_ERROR,
writeQueueSize: 0
writeQueueSize: 0,
trailersReady: false
};
this.on('resume', streamOnResume);
@ -1745,6 +1734,33 @@ class Http2Stream extends Duplex {
priorityFn();
}
sendTrailers(headers) {
if (this.destroyed || this.closed)
throw new ERR_HTTP2_INVALID_STREAM();
if (this[kSentTrailers])
throw new ERR_HTTP2_TRAILERS_ALREADY_SENT();
if (!this[kState].trailersReady)
throw new ERR_HTTP2_TRAILERS_NOT_READY();
assertIsObject(headers, 'headers');
headers = Object.assign(Object.create(null), headers);
const session = this[kSession];
debug(`Http2Stream ${this[kID]} [Http2Session ` +
`${sessionName(session[kType])}]: sending trailers`);
this[kUpdateTimer]();
const headersList = mapToHeaders(headers, assertValidPseudoHeaderTrailer);
if (!Array.isArray(headersList))
throw headersList;
this[kSentTrailers] = headers;
const ret = this[kHandle].trailers(headersList);
if (ret < 0)
this.destroy(new NghttpError(ret));
}
get closed() {
return !!(this[kState].flags & STREAM_FLAGS_CLOSED);
}
@ -2208,13 +2224,8 @@ class ServerHttp2Stream extends Http2Stream {
if (options.endStream)
streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
if (options.getTrailers !== undefined) {
if (typeof options.getTrailers !== 'function') {
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
}
if (options.waitForTrailers)
streamOptions |= STREAM_OPTION_GET_TRAILERS;
state.getTrailers = options.getTrailers;
}
headers = processHeaders(headers);
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
@ -2274,13 +2285,8 @@ class ServerHttp2Stream extends Http2Stream {
}
let streamOptions = 0;
if (options.getTrailers !== undefined) {
if (typeof options.getTrailers !== 'function') {
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
}
if (options.waitForTrailers)
streamOptions |= STREAM_OPTION_GET_TRAILERS;
this[kState].getTrailers = options.getTrailers;
}
if (typeof fd !== 'number')
throw new ERR_INVALID_ARG_TYPE('fd', 'number', fd);
@ -2340,13 +2346,8 @@ class ServerHttp2Stream extends Http2Stream {
}
let streamOptions = 0;
if (options.getTrailers !== undefined) {
if (typeof options.getTrailers !== 'function') {
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
}
if (options.waitForTrailers)
streamOptions |= STREAM_OPTION_GET_TRAILERS;
this[kState].getTrailers = options.getTrailers;
}
const session = this[kSession];
debug(`Http2Stream ${this[kID]} [Http2Session ` +

View File

@ -1066,16 +1066,6 @@ int Http2Session::OnNghttpError(nghttp2_session* handle,
return 0;
}
// Once all of the DATA frames for a Stream have been sent, the GetTrailers
// method calls out to JavaScript to fetch the trailing headers that need
// to be sent.
void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) {
if (!stream->IsDestroyed() && stream->HasTrailers()) {
Http2Stream::SubmitTrailers submit_trailers{this, stream, flags};
stream->OnTrailers(submit_trailers);
}
}
uv_buf_t Http2StreamListener::OnStreamAlloc(size_t size) {
// See the comments in Http2Session::OnDataChunkReceived
// (which is the only possible call site for this method).
@ -1111,25 +1101,6 @@ void Http2StreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
stream->CallJSOnreadMethod(nread, buffer);
}
Http2Stream::SubmitTrailers::SubmitTrailers(
Http2Session* session,
Http2Stream* stream,
uint32_t* flags)
: session_(session), stream_(stream), flags_(flags) { }
void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers,
size_t length) const {
Http2Scope h2scope(session_);
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);
}
// Called by OnFrameReceived to notify JavaScript land that a complete
// HEADERS frame has been received and processed. This method converts the
@ -1725,30 +1696,6 @@ nghttp2_stream* Http2Stream::operator*() {
return nghttp2_session_find_stream(**session_, id_);
}
// Calls out to JavaScript land to fetch the actual trailer headers to send
// for this stream.
void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) {
DEBUG_HTTP2STREAM(this, "prompting for trailers");
CHECK(!this->IsDestroyed());
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() && !IsDestroyed()) {
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::Close(int32_t code) {
CHECK(!this->IsDestroyed());
flags_ |= NGHTTP2_STREAM_FLAG_CLOSED;
@ -1843,6 +1790,26 @@ int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
return ret;
}
void Http2Stream::OnTrailers() {
DEBUG_HTTP2STREAM(this, "let javascript know we are ready for trailers");
CHECK(!this->IsDestroyed());
Isolate* isolate = env()->isolate();
HandleScope scope(isolate);
Local<Context> context = env()->context();
Context::Scope context_scope(context);
MakeCallback(env()->ontrailers_string(), 0, nullptr);
}
// Submit informational headers for a stream.
int Http2Stream::SubmitTrailers(nghttp2_nv* nva, size_t len) {
CHECK(!this->IsDestroyed());
Http2Scope h2scope(this);
DEBUG_HTTP2STREAM2(this, "sending %d trailers", len);
int ret = nghttp2_submit_trailer(**session_, id_, nva, len);
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
return ret;
}
// Submit a PRIORITY frame to the connected peer.
int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
bool silent) {
@ -2068,13 +2035,10 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
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);
// If the stream or session gets destroyed during the GetTrailers
// callback, check that here and close down the stream
if (stream->IsDestroyed())
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
if (session->IsDestroyed())
return NGHTTP2_ERR_CALLBACK_FAILURE;
if (stream->HasTrailers()) {
*flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
stream->OnTrailers();
}
}
stream->statistics_.sent_bytes += amount;
@ -2361,6 +2325,21 @@ void Http2Stream::Info(const FunctionCallbackInfo<Value>& args) {
headers->Length());
}
// Submits trailing headers on the Http2Stream
void Http2Stream::Trailers(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());
Local<Array> headers = args[0].As<Array>();
Headers list(isolate, context, headers);
args.GetReturnValue().Set(stream->SubmitTrailers(*list, list.length()));
DEBUG_HTTP2STREAM2(stream, "%d trailing headers sent", headers->Length());
}
// Grab the numeric id of the Http2Stream
void Http2Stream::GetID(const FunctionCallbackInfo<Value>& args) {
Http2Stream* stream;
@ -2706,6 +2685,7 @@ void Initialize(Local<Object> target,
env->SetProtoMethod(stream, "priority", Http2Stream::Priority);
env->SetProtoMethod(stream, "pushPromise", Http2Stream::PushPromise);
env->SetProtoMethod(stream, "info", Http2Stream::Info);
env->SetProtoMethod(stream, "trailers", Http2Stream::Trailers);
env->SetProtoMethod(stream, "respond", Http2Stream::Respond);
env->SetProtoMethod(stream, "rstStream", Http2Stream::RstStream);
env->SetProtoMethod(stream, "refreshState", Http2Stream::RefreshState);

View File

@ -581,6 +581,10 @@ class Http2Stream : public AsyncWrap,
// Submit informational headers for this stream
int SubmitInfo(nghttp2_nv* nva, size_t len);
// Submit trailing headers for this stream
int SubmitTrailers(nghttp2_nv* nva, size_t len);
void OnTrailers();
// Submit a PRIORITY frame for this stream
int SubmitPriority(nghttp2_priority_spec* prispec, bool silent = false);
@ -670,25 +674,6 @@ class Http2Stream : public AsyncWrap,
size_t self_size() const override { return sizeof(*this); }
// Handling Trailer Headers
class SubmitTrailers {
public:
void Submit(nghttp2_nv* trailers, size_t length) const;
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);
@ -697,6 +682,7 @@ class Http2Stream : public AsyncWrap,
static void PushPromise(const FunctionCallbackInfo<Value>& args);
static void RefreshState(const FunctionCallbackInfo<Value>& args);
static void Info(const FunctionCallbackInfo<Value>& args);
static void Trailers(const FunctionCallbackInfo<Value>& args);
static void Respond(const FunctionCallbackInfo<Value>& args);
static void RstStream(const FunctionCallbackInfo<Value>& args);
@ -859,8 +845,6 @@ class Http2Session : public AsyncWrap, public StreamListener {
size_t self_size() const override { return sizeof(*this); }
void GetTrailers(Http2Stream* stream, uint32_t* flags);
// Handle reads/writes from the underlying network transport.
void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override;
void OnStreamAfterWrite(WriteWrap* w, int status) override;

View File

@ -10,7 +10,6 @@ const http2 = require('http2');
const optionsToTest = {
endStream: 'boolean',
getTrailers: 'function',
weight: 'number',
parent: 'number',
exclusive: 'boolean',

View File

@ -50,15 +50,18 @@ server.listen(0, common.mustCall(function() {
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers, {
getTrailers(trailers) {
trailers['x-fOo'] = 'xOxOxOx';
trailers['x-foO'] = 'OxOxOxO';
trailers['X-fOo'] = 'xOxOxOx';
trailers['X-foO'] = 'OxOxOxO';
trailers['x-foo-test'] = 'test, test';
}
const request = client.request(headers, { waitForTrailers: true });
request.on('wantTrailers', () => {
request.sendTrailers({
'x-fOo': 'xOxOxOx',
'x-foO': 'OxOxOxO',
'X-fOo': 'xOxOxOx',
'X-foO': 'OxOxOxO',
'x-foo-test': 'test, test'
});
});
request.resume();
request.on('end', common.mustCall(function() {
server.close();

View File

@ -28,6 +28,15 @@ const server = h2.createServer((request, response) => {
}
);
response.stream.on('close', () => {
response.createPushResponse({
':path': '/pushed',
':method': 'GET'
}, common.mustCall((error) => {
assert.strictEqual(error.code, 'ERR_HTTP2_INVALID_STREAM');
}));
});
response.createPushResponse({
':path': '/pushed',
':method': 'GET'
@ -36,16 +45,6 @@ const server = h2.createServer((request, response) => {
assert.strictEqual(push.stream.id % 2, 0);
push.end(pushExpect);
response.end();
// wait for a tick, so the stream is actually closed
setImmediate(function() {
response.createPushResponse({
':path': '/pushed',
':method': 'GET'
}, common.mustCall((error) => {
assert.strictEqual(error.code, 'ERR_HTTP2_INVALID_STREAM');
}));
});
}));
});

View File

@ -20,15 +20,16 @@ server.on('stream', common.mustCall((stream) => {
});
});
stream.respond({}, {
getTrailers: common.mustCall((trailers) => {
trailers[':status'] = 'bar';
})
});
stream.respond({}, { waitForTrailers: true });
stream.on('error', common.expectsError({
code: 'ERR_HTTP2_INVALID_PSEUDOHEADER'
}));
stream.on('wantTrailers', () => {
common.expectsError(() => {
stream.sendTrailers({ ':status': 'bar' });
}, {
code: 'ERR_HTTP2_INVALID_PSEUDOHEADER'
});
stream.close();
});
stream.end('hello world');
}));
@ -38,12 +39,6 @@ server.listen(0, common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('error', common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR',
type: Error,
message: 'Stream closed with error code NGHTTP2_INTERNAL_ERROR'
}));
req.on('response', common.mustCall());
req.resume();
req.on('end', common.mustCall());

View File

@ -0,0 +1,35 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
const server = h2.createServer();
// we use the lower-level API here
server.on('stream', common.mustCall(onStream));
function onStream(stream, headers, flags) {
stream.respond(undefined, { waitForTrailers: true });
// There is no wantTrailers handler so this should close naturally
// without hanging. If the test completes without timing out, then
// it passes.
stream.end('ok');
}
server.listen(0);
server.on('listening', common.mustCall(function() {
const client = h2.connect(`http://localhost:${this.address().port}`);
const req = client.request();
req.resume();
req.on('trailers', common.mustCall((headers) => {
assert.strictEqual(Object.keys(headers).length, 0);
}));
req.on('close', common.mustCall(() => {
server.close();
client.close();
}));
}));

View File

@ -7,48 +7,13 @@ if (!common.hasCrypto)
const http2 = require('http2');
const { Http2Stream } = process.binding('http2');
const types = {
boolean: true,
function: () => {},
number: 1,
object: {},
array: [],
null: null,
symbol: Symbol('test')
};
const server = http2.createServer();
Http2Stream.prototype.respond = () => 1;
server.on('stream', common.mustCall((stream) => {
// Check for all possible TypeError triggers on options.getTrailers
Object.entries(types).forEach(([type, value]) => {
if (type === 'function') {
return;
}
common.expectsError(
() => stream.respond({
'content-type': 'text/plain'
}, {
['getTrailers']: value
}),
{
type: TypeError,
code: 'ERR_INVALID_OPT_VALUE',
message: `The value "${String(value)}" is invalid ` +
'for option "getTrailers"'
}
);
});
// Send headers
stream.respond({
'content-type': 'text/plain'
}, {
['getTrailers']: () => common.mustCall()
});
stream.respond({ 'content-type': 'text/plain' });
// Should throw if headers already sent
common.expectsError(

View File

@ -9,8 +9,7 @@ const http2 = require('http2');
const optionsWithTypeError = {
offset: 'number',
length: 'number',
statCheck: 'function',
getTrailers: 'function'
statCheck: 'function'
};
const types = {

View File

@ -10,8 +10,7 @@ const fs = require('fs');
const optionsWithTypeError = {
offset: 'number',
length: 'number',
statCheck: 'function',
getTrailers: 'function'
statCheck: 'function'
};
const types = {

View File

@ -12,10 +12,9 @@ server.on('stream', common.mustCall((stream) => {
stream.additionalHeaders({ ':status': 102 });
assert.strictEqual(stream.sentInfoHeaders[0][':status'], 102);
stream.respond({ abc: 'xyz' }, {
getTrailers(headers) {
headers.xyz = 'abc';
}
stream.respond({ abc: 'xyz' }, { waitForTrailers: true });
stream.on('wantTrailers', () => {
stream.sendTrailers({ xyz: 'abc' });
});
assert.strictEqual(stream.sentHeaders.abc, 'xyz');
assert.strictEqual(stream.sentHeaders[':status'], 200);

View File

@ -22,11 +22,26 @@ function onStream(stream, headers, flags) {
stream.respond({
'content-type': 'text/html',
':status': 200
}, {
getTrailers: common.mustCall((trailers) => {
trailers[trailerKey] = trailerValue;
})
}, { waitForTrailers: true });
stream.on('wantTrailers', () => {
stream.sendTrailers({ [trailerKey]: trailerValue });
common.expectsError(
() => stream.sendTrailers({}),
{
code: 'ERR_HTTP2_TRAILERS_ALREADY_SENT',
type: Error
}
);
});
common.expectsError(
() => stream.sendTrailers({}),
{
code: 'ERR_HTTP2_TRAILERS_NOT_READY',
type: Error
}
);
stream.end(body);
}
@ -34,16 +49,23 @@ server.listen(0);
server.on('listening', common.mustCall(function() {
const client = h2.connect(`http://localhost:${this.address().port}`);
const req = client.request({ ':path': '/', ':method': 'POST' }, {
getTrailers: common.mustCall((trailers) => {
trailers[trailerKey] = trailerValue;
})
const req = client.request({ ':path': '/', ':method': 'POST' },
{ waitForTrailers: true });
req.on('wantTrailers', () => {
req.sendTrailers({ [trailerKey]: trailerValue });
});
req.on('data', common.mustCall());
req.on('trailers', common.mustCall((headers) => {
assert.strictEqual(headers[trailerKey], trailerValue);
}));
req.on('end', common.mustCall(() => {
req.on('close', common.mustCall(() => {
common.expectsError(
() => req.sendTrailers({}),
{
code: 'ERR_HTTP2_INVALID_STREAM',
type: Error
}
);
server.close();
client.close();
}));