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:
parent
2e76b175ed
commit
237aa7e9ae
@ -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
|
||||
|
||||
|
191
doc/api/http2.md
191
doc/api/http2.md
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 ` +
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -10,7 +10,6 @@ const http2 = require('http2');
|
||||
|
||||
const optionsToTest = {
|
||||
endStream: 'boolean',
|
||||
getTrailers: 'function',
|
||||
weight: 'number',
|
||||
parent: 'number',
|
||||
exclusive: 'boolean',
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
}));
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -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());
|
||||
|
35
test/parallel/test-http2-no-wanttrailers-listener.js
Normal file
35
test/parallel/test-http2-no-wanttrailers-listener.js
Normal 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();
|
||||
}));
|
||||
}));
|
@ -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(
|
||||
|
@ -9,8 +9,7 @@ const http2 = require('http2');
|
||||
const optionsWithTypeError = {
|
||||
offset: 'number',
|
||||
length: 'number',
|
||||
statCheck: 'function',
|
||||
getTrailers: 'function'
|
||||
statCheck: 'function'
|
||||
};
|
||||
|
||||
const types = {
|
||||
|
@ -10,8 +10,7 @@ const fs = require('fs');
|
||||
const optionsWithTypeError = {
|
||||
offset: 'number',
|
||||
length: 'number',
|
||||
statCheck: 'function',
|
||||
getTrailers: 'function'
|
||||
statCheck: 'function'
|
||||
};
|
||||
|
||||
const types = {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}));
|
||||
|
Loading…
x
Reference in New Issue
Block a user