diff --git a/doc/api/errors.md b/doc/api/errors.md index ac110a77445343..73fd1d52cbeedc 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -758,6 +758,21 @@ Status code was outside the regular status code range (100-999). The `Trailer` header was set even though the transfer encoding does not support that. + +### ERR_HTTP2_ALREADY_SHUTDOWN + +Occurs with multiple attempts to shutdown an HTTP/2 session. + + +### ERR_HTTP2_ALTSVC_INVALID_ORIGIN + +HTTP/2 ALTSVC frames require a valid origin. + + +### ERR_HTTP2_ALTSVC_LENGTH + +HTTP/2 ALTSVC frames are limited to a maximum of 16,382 payload bytes. + ### ERR_HTTP2_CONNECT_AUTHORITY @@ -781,6 +796,12 @@ forbidden. A failure occurred sending an individual frame on the HTTP/2 session. + +### ERR_HTTP2_GOAWAY_SESSION + +New HTTP/2 Streams may not be opened after the `Http2Session` has received a +`GOAWAY` frame from the connected peer. + ### ERR_HTTP2_HEADER_REQUIRED @@ -869,7 +890,7 @@ An operation was performed on a stream that had already been destroyed. ### ERR_HTTP2_MAX_PENDING_SETTINGS_ACK Whenever an HTTP/2 `SETTINGS` frame is sent to a connected peer, the peer is -required to send an acknowledgement that it has received and applied the new +required to send an acknowledgment that it has received and applied the new `SETTINGS`. By default, a maximum number of unacknowledged `SETTINGS` frames may be sent at any given time. This error code is used when that limit has been reached. @@ -895,7 +916,7 @@ forbidden. ### ERR_HTTP2_PING_CANCEL -An HTTP/2 ping was cancelled. +An HTTP/2 ping was canceled. ### ERR_HTTP2_PING_LENGTH @@ -920,6 +941,11 @@ client. An attempt was made to use the `Http2Stream.prototype.responseWithFile()` API to send something other than a regular file. + +### ERR_HTTP2_SESSION_ERROR + +The `Http2Session` closed with a non-zero error code. + ### ERR_HTTP2_SOCKET_BOUND @@ -937,10 +963,11 @@ Use of the `101` Informational status code is forbidden in HTTP/2. An invalid HTTP status code has been specified. Status codes must be an integer between `100` and `599` (inclusive). - -### ERR_HTTP2_STREAM_CLOSED + +### ERR_HTTP2_STREAM_CANCEL -An action was performed on an HTTP/2 Stream that had already been closed. +An `Http2Stream` was destroyed before any data was transmitted to the connected +peer. ### ERR_HTTP2_STREAM_ERROR @@ -1222,11 +1249,11 @@ A failure occurred resolving imports in an [ES6 module][]. ### ERR_MULTIPLE_CALLBACK -A callback was called more then once. +A callback was called more than once. *Note*: A callback is almost always meant to only be called once as the query can either be fulfilled or rejected but not both at the same time. The latter -would be possible by calling a callback more then once. +would be possible by calling a callback more than once. ### ERR_NAPI_CONS_FUNCTION diff --git a/doc/api/http2.md b/doc/api/http2.md index 0d298abb8057b7..9ef3fe27413ab7 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -67,8 +67,8 @@ const fs = require('fs'); const client = http2.connect('https://localhost:8443', { ca: fs.readFileSync('localhost-cert.pem') }); -client.on('socketError', (err) => console.error(err)); client.on('error', (err) => console.error(err)); +client.on('socketError', (err) => console.error(err)); const req = client.request({ ':path': '/' }); @@ -83,7 +83,7 @@ let data = ''; req.on('data', (chunk) => { data += chunk; }); req.on('end', () => { console.log(`\n${data}`); - client.destroy(); + client.close(); }); req.end(); ``` @@ -127,7 +127,7 @@ solely on the API of the `Http2Session`. added: v8.4.0 --> -The `'close'` event is emitted once the `Http2Session` has been terminated. +The `'close'` event is emitted once the `Http2Session` has been destroyed. #### Event: 'connect' -The `'localSettings'` event is emitted when an acknowledgement 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. @@ -234,19 +234,13 @@ of the stream. ```js const http2 = require('http2'); -const { - HTTP2_HEADER_METHOD, - HTTP2_HEADER_PATH, - HTTP2_HEADER_STATUS, - HTTP2_HEADER_CONTENT_TYPE -} = http2.constants; session.on('stream', (stream, headers, flags) => { - const method = headers[HTTP2_HEADER_METHOD]; - const path = headers[HTTP2_HEADER_PATH]; + const method = headers[':method']; + const path = headers[':path']; // ... stream.respond({ - [HTTP2_HEADER_STATUS]: 200, - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + ':status': 200, + 'content-type': 'text/plain' }); stream.write('hello '); stream.end('world'); @@ -275,19 +269,6 @@ server.on('stream', (stream, headers) => { server.listen(80); ``` -#### Event: 'socketError' - - -The `'socketError'` event is emitted when an `'error'` is emitted on the -`Socket` instance bound to the `Http2Session`. If this event is not handled, -the `'error'` event will be re-emitted on the `Socket`. - -For `ServerHttp2Session` instances, a `'socketError'` event listener is always -registered that will, by default, forward the event on to the owning -`Http2Server` instance if no additional handlers are registered. - #### Event: 'timeout' + +* Value: {string|undefined} + +Value will be `undefined` if the `Http2Session` is not yet connected to a +socket, `h2c` if the `Http2Session` is not connected to a `TLSSocket`, or +will return the value of the connected `TLSSocket`'s own `alpnProtocol` +property. + +#### http2session.close([callback]) + + +* `callback` {Function} + +Gracefully closes the `Http2Session`, allowing any existing streams to +complete on their own and preventing new `Http2Stream` instances from being +created. Once closed, `http2session.destroy()` *might* be called if there +are no open `Http2Stream` instances. + +If specified, the `callback` function is registered as a handler for the +`'close'` event. + +#### http2session.closed + + +* Value: {boolean} + +Will be `true` if this `Http2Session` instance has been closed, otherwise +`false`. + +#### http2session.destroy([error,][code]) +* `error` {Error} An `Error` object if the `Http2Session` is being destroyed + due to an error. +* `code` {number} The HTTP/2 error code to send in the final `GOAWAY` frame. + If unspecified, and `error` is not undefined, the default is `INTERNAL_ERROR`, + otherwise defaults to `NO_ERROR`. * Returns: {undefined} Immediately terminates the `Http2Session` and the associated `net.Socket` or `tls.TLSSocket`. +Once destroyed, the `Http2Session` will emit the `'close'` event. If `error` +is not undefined, an `'error'` event will be emitted immediately after the +`'close'` event. + +If there are any remaining open `Http2Streams` associated with the +`Http2Session`, those will also be destroyed. + #### http2session.destroyed + +* Value: {boolean|undefined} + +Value is `undefined` if the `Http2Session` session socket has not yet been +connected, `true` if the `Http2Session` is connected with a `TLSSocket`, +and `false` if the `Http2Session` is connected to any other kind of socket +or stream. + +#### http2session.goaway([code, [lastStreamID, [opaqueData]]]) + + +* `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. + +Transmits a `GOAWAY` frame to the connected peer *without* shutting down the +`Http2Session`. + #### http2session.localSettings + +* Value: {string[]|undefined} + +If the `Http2Session` is connected to a `TLSSocket`, the `originSet` property +will return an Array of origins for which the `Http2Session` may be +considered authoritative. + #### http2session.pendingSettingsAck -* Value: {[Settings Object][]} +Calls [`ref()`][`net.Socket.prototype.ref`] on this `Http2Session` +instance's underlying [`net.Socket`]. -A prototype-less object describing the current remote settings of this -`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer. - -#### http2session.request(headers[, options]) +#### http2session.remoteSettings -* `headers` {[Headers Object][]} -* `options` {Object} - * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should - be closed initially, such as when sending a `GET` request that should not - expect a payload body. - * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, - the created stream is made the sole direct dependency of the parent, with - all other existing dependents made a dependent of the newly created stream. - **Default:** `false` - * `parent` {number} Specifies the numeric identifier of a stream the newly - created 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). - * `getTrailers` {Function} Callback function invoked to collect trailer - headers. - -* Returns: {ClientHttp2Stream} - -For HTTP/2 Client `Http2Session` instances only, the `http2session.request()` -creates and returns an `Http2Stream` instance that can be used to send an -HTTP/2 request to the connected server. - -This method is only available if `http2session.type` is equal to -`http2.constants.NGHTTP2_SESSION_CLIENT`. - -```js -const http2 = require('http2'); -const clientSession = http2.connect('https://localhost:1234'); -const { - HTTP2_HEADER_PATH, - HTTP2_HEADER_STATUS -} = http2.constants; - -const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' }); -req.on('response', (headers) => { - console.log(headers[HTTP2_HEADER_STATUS]); - req.on('data', (chunk) => { /** .. **/ }); - req.on('end', () => { /** .. **/ }); -}); -``` - -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 used to specify -the trailing header fields to send to the peer. +* Value: {[Settings Object][]} -*Note*: 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. +A prototype-less object describing the current remote settings of this +`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer. #### http2session.setTimeout(msecs, callback) * `options` {Object} - * `graceful` {boolean} `true` to attempt a polite shutdown of the - `Http2Session`. * `errorCode` {number} The HTTP/2 [error code][] to return. Note that this is *not* the same thing as an HTTP Response Status Code. **Default:** `0x00` (No Error). * `lastStreamID` {number} The Stream ID of the last successfully processed - `Http2Stream` on this `Http2Session`. + `Http2Stream` on this `Http2Session`. If unspecified, will default to the + ID of the most recently received stream. * `opaqueData` {Buffer|Uint8Array} A `Buffer` or `Uint8Array` instance containing arbitrary additional data to send to the peer upon disconnection. This is used, typically, to provide additional data for debugging failures, @@ -483,23 +499,20 @@ added: v8.4.0 has been completed. * Returns: {undefined} -Attempts to shutdown this `Http2Session` using HTTP/2 defined procedures. +Attempts to shut down this `Http2Session` using HTTP/2 defined procedures. If specified, the given `callback` function will be invoked once the shutdown process has completed. -Note that calling `http2session.shutdown()` does *not* destroy the session or -tear down the `Socket` connection. It merely prompts both sessions to begin -preparing to cease activity. - -During a "graceful" shutdown, the session will first send a `GOAWAY` frame to -the connected peer identifying the last processed stream as 232-1. +If the `Http2Session` instance is a server-side session and the `errorCode` +option is `0x00` (No Error), a "graceful" shutdown will be initiated. During a +"graceful" shutdown, the session will first send a `GOAWAY` frame to +the connected peer identifying the last processed stream as 231-1. Then, on the next tick of the event loop, a second `GOAWAY` frame identifying the most recently processed stream identifier is sent. This process allows the remote peer to begin preparing for the connection to be terminated. ```js -session.shutdown({ - graceful: true, +session.close({ opaqueData: Buffer.from('add some debugging data here') }, () => session.destroy()); ``` @@ -556,8 +569,8 @@ while the session is waiting for the remote peer to acknowledge the new settings. *Note*: The new settings will not become effective until the SETTINGS -acknowledgement is received and the `'localSettings'` event is emitted. It -is possible to send multiple SETTINGS frames while acknowledgement is still +acknowledgment is received and the `'localSettings'` event is emitted. It +is possible to send multiple SETTINGS frames while acknowledgment is still pending. #### http2session.type @@ -572,6 +585,174 @@ The `http2session.type` will be equal to server, and `http2.constants.NGHTTP2_SESSION_CLIENT` if the instance is a client. +#### http2session.unref() + + +Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session` +instance's underlying [`net.Socket`]. + +### Class: ServerHttp2Session + + +#### serverhttp2session.altsvc(alt, originOrStream) + + +* `alt` {string} A description of the alternative service configuration as + defined by [RFC 7838][]. +* `originOrStream` {number|string|URL|Object} Either a URL string specifying + the origin (or an Object with an `origin` property) or the numeric identifier + of an active `Http2Stream` as given by the `http2stream.id` property. + +Submits an `ALTSVC` frame (as defined by [RFC 7838][]) to the connected client. + +```js +const http2 = require('http2'); + +const server = http2.createServer(); +server.on('session', (session) => { + // Set altsvc for origin https://example.org:80 + session.altsvc('h2=":8000"', 'https://example.org:80'); +}); + +server.on('stream', (stream) => { + // Set altsvc for a specific stream + stream.session.altsvc('h2=":8000"', stream.id); +}); +``` + +Sending an `ALTSVC` frame with a specific stream ID indicates that the alternate +service is associated with the origin of the given `Http2Stream`. + +The `alt` and origin string *must* contain only ASCII bytes and are +strictly interpreted as a sequence of ASCII bytes. The special value `'clear'` +may be passed to clear any previously set alternative service for a given +domain. + +When a string is passed for the `originOrStream` argument, it will be parsed as +a URL and the origin will be derived. For instance, the origin for the +HTTP URL `'https://example.org/foo/bar'` is the ASCII string +`'https://example.org'`. An error will be thrown if either the given string +cannot be parsed as a URL or if a valid origin cannot be derived. + +A `URL` object, or any object with an `origin` property, may be passed as +`originOrStream`, in which case the value of the `origin` property will be +used. The value of the `origin` property *must* be a properly serialized +ASCII origin. + +#### Specifying alternative services + +The format of the `alt` parameter is strictly defined by [RFC 7838][] as an +ASCII string containing a comma-delimited list of "alternative" protocols +associated with a specific host and port. + +For example, the value `'h2="example.org:81"'` indicates that the HTTP/2 +protocol is available on the host `'example.org'` on TCP/IP port 81. The +host and port *must* be contained within the quote (`"`) characters. + +Multiple alternatives may be specified, for instance: `'h2="example.org:81", +h2=":82"'` + +The protocol identifier (`'h2'` in the examples) may be any valid +[ALPN Protocol ID][]. + +The syntax of these values is not validated by the Node.js implementation and +are passed through as provided by the user or received from the peer. + +### Class: ClientHttp2Session + + +#### Event: 'altsvc' + + +The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by +the client. The event is emitted with the `ALTSVC` value, origin, and stream +ID, if any. If no `origin` is provided in the `ALTSVC` frame, `origin` will +be an empty string. + +```js +const http2 = require('http2'); +const client = http2.connect('https://example.org'); + +client.on('altsvc', (alt, origin, stream) => { + console.log(alt); + console.log(origin); + console.log(stream); +}); +``` + +#### clienthttp2session.request(headers[, options]) + + +* `headers` {[Headers Object][]} +* `options` {Object} + * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should + be closed initially, such as when sending a `GET` request that should not + expect a payload body. + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + the created stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of the newly created stream. + **Default:** `false` + * `parent` {number} Specifies the numeric identifier of a stream the newly + created 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). + * `getTrailers` {Function} Callback function invoked to collect trailer + headers. + +* Returns: {ClientHttp2Stream} + +For HTTP/2 Client `Http2Session` instances only, the `http2session.request()` +creates and returns an `Http2Stream` instance that can be used to send an +HTTP/2 request to the connected server. + +This method is only available if `http2session.type` is equal to +`http2.constants.NGHTTP2_SESSION_CLIENT`. + +```js +const http2 = require('http2'); +const clientSession = http2.connect('https://localhost:1234'); +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS +} = http2.constants; + +const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' }); +req.on('response', (headers) => { + console.log(headers[HTTP2_HEADER_STATUS]); + req.on('data', (chunk) => { /** .. **/ }); + req.on('end', () => { /** .. **/ }); +}); +``` + +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. + +*Note*: 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. + +The `:method` and `:path` pseudo-headers are not specified within `headers`, +they respectively default to: + +* `:method` = `'GET'` +* `:path` = `/` + ### Class: Http2Stream The `'timeout'` event is emitted after no activity is received for this -`'Http2Stream'` within the number of millseconds set using +`'Http2Stream'` within the number of milliseconds set using `http2stream.setTimeout()`. #### Event: 'trailers' @@ -720,6 +901,29 @@ added: v8.4.0 Set to `true` if the `Http2Stream` instance was aborted abnormally. When set, the `'aborted'` event will have been emitted. +#### http2stream.close(code[, callback]) + + +* code {number} Unsigned 32-bit integer identifying the error code. **Default:** + `http2.constant.NGHTTP2_NO_ERROR` (`0x00`) +* `callback` {Function} An optional function registered to listen for the + `'close'` event. +* Returns: {undefined} + +Closes the `Http2Stream` instance by sending an `RST_STREAM` frame to the +connected HTTP/2 peer. + +#### http2stream.closed + + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance has been closed. + #### http2stream.destroyed + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance has not yet been assigned a +numeric stream identifier. + #### http2stream.priority(options) - -* 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 this -`Http2Stream` to be closed on both sides using [error code][] `code`. - -#### http2stream.rstWithNoError() - - -* Returns: {undefined} - -Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error). - -#### http2stream.rstWithProtocolError() - - -* Returns: {undefined} - -Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error). - -#### http2stream.rstWithCancel() - - -* Returns: {undefined} - -Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel). - -#### http2stream.rstWithRefuse() - - -* Returns: {undefined} - -Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream). - -#### http2stream.rstWithInternalError() - - -* Returns: {undefined} - -Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error). - #### http2stream.session The `'headers'` event is emitted when an additional block of headers is received -for a stream, such as when a block of `1xx` informational headers are received. +for a stream, such as when a block of `1xx` informational headers is received. The listener callback is passed the [Headers Object][] and flags associated with the headers. @@ -1041,7 +1198,7 @@ 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 used to specify +single object (with a `null` prototype) that the listener may use to specify the trailing header fields to send to the peer. ```js @@ -1058,7 +1215,7 @@ server.on('stream', (stream) => { ``` *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2 -"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event +pseudo-header fields (e.g. `':status'`, `':path'`, etc). An `'error'` event will be emitted if the `getTrailers` callback attempts to set such header fields. @@ -1115,7 +1272,7 @@ requests. 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 used to specify +single object (with a `null` prototype) that the listener may use to specify the trailing header fields to send to the peer. ```js @@ -1142,7 +1299,7 @@ server.on('close', () => fs.closeSync(fd)); ``` *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2 -"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event +pseudo-header fields (e.g. `':status'`, `':path'`, etc). An `'error'` event will be emitted if the `getTrailers` callback attempts to set such header fields. @@ -1174,7 +1331,7 @@ of the given file: If an error occurs while attempting to read the file data, the `Http2Stream` will be closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR` -code. If the `onError` callback is defined it will be called, otherwise +code. If the `onError` callback is defined, then it will be called. Otherwise the stream will be destroyed. Example using a file path: @@ -1234,7 +1391,7 @@ 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 used to specify +single object (with a `null` prototype) that the listener may use to specify the trailing header fields to send to the peer. ```js @@ -1251,7 +1408,7 @@ server.on('stream', (stream) => { ``` *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2 -"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event +pseudo-header fields (e.g. `':status'`, `':path'`, etc). An `'error'` event will be emitted if the `getTrailers` callback attempts to set such header fields. @@ -1264,16 +1421,49 @@ added: v8.4.0 In `Http2Server`, there is no `'clientError'` event as there is in HTTP1. However, there are `'socketError'`, `'sessionError'`, and -`'streamError'`, for error happened on the socket, session, or stream -respectively. +`'streamError'`, for errors emitted on the socket, `Http2Session`, or +`Http2Stream`. + +#### Event: 'checkContinue' + + +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +If a [`'request'`][] listener is registered or [`http2.createServer()`][] is +supplied a callback function, the `'checkContinue'` event is emitted each time +a request with an HTTP `Expect: 100-continue` is received. If this event is +not listened for, the server will automatically respond with a status +`100 Continue` as appropriate. + +Handling this event involves calling [`response.writeContinue()`][] if the client +should continue to send the request body, or generating an appropriate HTTP +response (e.g. 400 Bad Request) if the client should not continue to send the +request body. -#### Event: 'socketError' +Note that when this event is emitted and handled, the [`'request'`][] event will +not be emitted. + +#### Event: 'request' -The `'socketError'` event is emitted when a `'socketError'` event is emitted by -an `Http2Session` associated with the server. +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API][]. + +#### Event: 'session' + + +The `'session'` event is emitted when a new `Http2Session` is created by the +`Http2Server`. #### Event: 'sessionError' The `'sessionError'` event is emitted when an `'error'` event is emitted by -an `Http2Session` object. If no listener is registered for this event, an -`'error'` event is emitted. +an `Http2Session` object associated with the `Http2Server`. #### Event: 'streamError' -* `socket` {http2.ServerHttp2Stream} - -If an `ServerHttp2Stream` emits an `'error'` event, it will be forwarded here. +If a `ServerHttp2Stream` emits an `'error'` event, it will be forwarded here. The stream will already be destroyed when this event is triggered. #### Event: 'stream' @@ -1325,17 +1512,6 @@ server.on('stream', (stream, headers, flags) => { }); ``` -#### Event: 'request' - - -* `request` {http2.Http2ServerRequest} -* `response` {http2.Http2ServerResponse} - -Emitted each time there is a request. Note that there may be multiple requests -per session. See the [Compatibility API][]. - #### Event: 'timeout' - -* `request` {http2.Http2ServerRequest} -* `response` {http2.Http2ServerResponse} - -If a [`'request'`][] listener is registered or [`http2.createServer()`][] is -supplied a callback function, the `'checkContinue'` event is emitted each time -a request with an HTTP `Expect: 100-continue` is received. If this event is -not listened for, the server will automatically respond with a status -`100 Continue` as appropriate. - -Handling this event involves calling [`response.writeContinue()`][] if the client -should continue to send the request body, or generating an appropriate HTTP -response (e.g. 400 Bad Request) if the client should not continue to send the -request body. - -Note that when this event is emitted and handled, the [`'request'`][] event will -not be emitted. - ### Class: Http2SecureServer The `'sessionError'` event is emitted when an `'error'` event is emitted by -an `Http2Session` object. If no listener is registered for this event, an -`'error'` event is emitted on the `Http2Session` instance instead. - -#### Event: 'socketError' - - -The `'socketError'` event is emitted when a `'socketError'` event is emitted by -an `Http2Session` associated with the server. +an `Http2Session` object associated with the `Http2SecureServer`. #### Event: 'unknownProtocol' + + + +```js +const http2 = require('../common/http2'); +``` + +### Class: Frame + +The `http2.Frame` is a base class that creates a `Buffer` containing a +serialized HTTP/2 frame header. + + + + + +```js +// length is a 24-bit unsigned integer +// type is an 8-bit unsigned integer identifying the frame type +// flags is an 8-bit unsigned integer containing the flag bits +// id is the 32-bit stream identifier, if any. +const frame = new http2.Frame(length, type, flags, id); + +// Write the frame data to a socket +socket.write(frame.data); +``` + +The serialized `Buffer` may be retrieved using the `frame.data` property. + +### Class: DataFrame extends Frame + +The `http2.DataFrame` is a subclass of `http2.Frame` that serializes a `DATA` +frame. + + + + + +```js +// id is the 32-bit stream identifier +// payload is a Buffer containing the DATA payload +// padlen is an 8-bit integer giving the number of padding bytes to include +// final is a boolean indicating whether the End-of-stream flag should be set, +// defaults to false. +const data = new http2.DataFrame(id, payload, padlen, final); + +socket.write(frame.data); +``` + +### Class: HeadersFrame + +The `http2.HeadersFrame` is a subclass of `http2.Frame` that serializes a +`HEADERS` frame. + + + + + +```js +// id is the 32-bit stream identifier +// payload is a Buffer containing the HEADERS payload (see either +// http2.kFakeRequestHeaders or http2.kFakeResponseHeaders). +// padlen is an 8-bit integer giving the number of padding bytes to include +// final is a boolean indicating whether the End-of-stream flag should be set, +// defaults to false. +const data = new http2.HeadersFrame(id, http2.kFakeRequestHeaders, + padlen, final); + +socket.write(frame.data); +``` + +### Class: SettingsFrame + +The `http2.SettingsFrame` is a subclass of `http2.Frame` that serializes an +empty `SETTINGS` frame. + + + + + +```js +// ack is a boolean indicating whether or not to set the ACK flag. +const frame = new http2.SettingsFrame(ack); + +socket.write(frame.data); +``` + +### http2.kFakeRequestHeaders + +Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2 +request headers to be used as the payload of a `http2.HeadersFrame`. + + + + + +```js +const frame = new http2.HeadersFrame(1, http2.kFakeRequestHeaders, 0, true); + +socket.write(frame.data); +``` + +### http2.kFakeResponseHeaders + +Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2 +response headers to be used as the payload a `http2.HeadersFrame`. + + + + + +```js +const frame = new http2.HeadersFrame(1, http2.kFakeResponseHeaders, 0, true); + +socket.write(frame.data); +``` + +### http2.kClientMagic + +Set to a `Buffer` containing the preamble bytes an HTTP/2 client must send +upon initial establishment of a connection. + + + + + +```js +socket.write(http2.kClientMagic); +``` + + [<Array>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array [<ArrayBufferView[]>]: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView [<Boolean>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type diff --git a/test/common/http2.js b/test/common/http2.js new file mode 100644 index 00000000000000..1d4c269fffd5b5 --- /dev/null +++ b/test/common/http2.js @@ -0,0 +1,139 @@ +/* eslint-disable required-modules */ +'use strict'; + +// An HTTP/2 testing tool used to create mock frames for direct testing +// of HTTP/2 endpoints. + +const kFrameData = Symbol('frame-data'); +const FLAG_EOS = 0x1; +const FLAG_ACK = 0x1; +const FLAG_EOH = 0x4; +const FLAG_PADDED = 0x8; +const PADDING = Buffer.alloc(255); + +const kClientMagic = Buffer.from('505249202a20485454502f322' + + 'e300d0a0d0a534d0d0a0d0a', 'hex'); + +const kFakeRequestHeaders = Buffer.from('828684410f7777772e65' + + '78616d706c652e636f6d', 'hex'); + + +const kFakeResponseHeaders = Buffer.from('4803333032580770726976617465611d' + + '4d6f6e2c203231204f63742032303133' + + '2032303a31333a323120474d546e1768' + + '747470733a2f2f7777772e6578616d70' + + '6c652e636f6d', 'hex'); + +function isUint32(val) { + return val >>> 0 === val; +} + +function isUint24(val) { + return val >>> 0 === val && val <= 0xFFFFFF; +} + +function isUint8(val) { + return val >>> 0 === val && val <= 0xFF; +} + +function write32BE(array, pos, val) { + if (!isUint32(val)) + throw new RangeError('val is not a 32-bit number'); + array[pos++] = (val >> 24) & 0xff; + array[pos++] = (val >> 16) & 0xff; + array[pos++] = (val >> 8) & 0xff; + array[pos++] = val & 0xff; +} + +function write24BE(array, pos, val) { + if (!isUint24(val)) + throw new RangeError('val is not a 24-bit number'); + array[pos++] = (val >> 16) & 0xff; + array[pos++] = (val >> 8) & 0xff; + array[pos++] = val & 0xff; +} + +function write8(array, pos, val) { + if (!isUint8(val)) + throw new RangeError('val is not an 8-bit number'); + array[pos] = val; +} + +class Frame { + constructor(length, type, flags, id) { + this[kFrameData] = Buffer.alloc(9); + write24BE(this[kFrameData], 0, length); + write8(this[kFrameData], 3, type); + write8(this[kFrameData], 4, flags); + write32BE(this[kFrameData], 5, id); + } + + get data() { + return this[kFrameData]; + } +} + +class SettingsFrame extends Frame { + constructor(ack = false) { + let flags = 0; + if (ack) + flags |= FLAG_ACK; + super(0, 4, flags, 0); + } +} + +class DataFrame extends Frame { + constructor(id, payload, padlen = 0, final = false) { + let len = payload.length; + let flags = 0; + if (final) flags |= FLAG_EOS; + const buffers = [payload]; + if (padlen > 0) { + buffers.unshift(Buffer.from([padlen])); + buffers.push(PADDING.slice(0, padlen)); + len += padlen + 1; + flags |= FLAG_PADDED; + } + super(len, 0, flags, id); + buffers.unshift(this[kFrameData]); + this[kFrameData] = Buffer.concat(buffers); + } +} + +class HeadersFrame extends Frame { + constructor(id, payload, padlen = 0, final = false) { + let len = payload.length; + let flags = FLAG_EOH; + if (final) flags |= FLAG_EOS; + const buffers = [payload]; + if (padlen > 0) { + buffers.unshift(Buffer.from([padlen])); + buffers.push(PADDING.slice(0, padlen)); + len += padlen + 1; + flags |= FLAG_PADDED; + } + super(len, 1, flags, id); + buffers.unshift(this[kFrameData]); + this[kFrameData] = Buffer.concat(buffers); + } +} + +class PingFrame extends Frame { + constructor(ack = false) { + const buffers = [Buffer.alloc(8)]; + super(8, 6, ack ? 1 : 0, 0); + buffers.unshift(this[kFrameData]); + this[kFrameData] = Buffer.concat(buffers); + } +} + +module.exports = { + Frame, + DataFrame, + HeadersFrame, + SettingsFrame, + PingFrame, + kFakeRequestHeaders, + kFakeResponseHeaders, + kClientMagic +}; diff --git a/test/parallel/test-http2-altsvc.js b/test/parallel/test-http2-altsvc.js new file mode 100644 index 00000000000000..9fd9a9fc278552 --- /dev/null +++ b/test/parallel/test-http2-altsvc.js @@ -0,0 +1,126 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); +const { URL } = require('url'); +const Countdown = require('../common/countdown'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.session.altsvc('h2=":8000"', stream.id); + stream.respond(); + stream.end('ok'); +})); +server.on('session', common.mustCall((session) => { + // Origin may be specified by string, URL object, or object with an + // origin property. For string and URL object, origin is guaranteed + // to be an ASCII serialized origin. For object with an origin + // property, it is up to the user to ensure proper serialization. + session.altsvc('h2=":8000"', 'https://example.org:8111/this'); + session.altsvc('h2=":8000"', new URL('https://example.org:8111/this')); + session.altsvc('h2=":8000"', { origin: 'https://example.org:8111' }); + + // Won't error, but won't send anything because the stream does not exist + session.altsvc('h2=":8000"', 3); + + // Will error because the numeric stream id is out of valid range + [0, -1, 1.1, 0xFFFFFFFF + 1, Infinity, -Infinity].forEach((i) => { + common.expectsError( + () => session.altsvc('h2=":8000"', i), + { + code: 'ERR_OUT_OF_RANGE', + type: RangeError + } + ); + }); + + // First argument must be a string + [0, {}, [], null, Infinity].forEach((i) => { + common.expectsError( + () => session.altsvc(i), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + } + ); + }); + + ['\u0001', 'h2="\uff20"', '👀'].forEach((i) => { + common.expectsError( + () => session.altsvc(i), + { + code: 'ERR_INVALID_CHAR', + type: TypeError + } + ); + }); + + [{}, [], true].forEach((i) => { + common.expectsError( + () => session.altsvc('clear', i), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + } + ); + }); + + [ + 'abc:', + new URL('abc:'), + { origin: 'null' }, + { origin: '' } + ].forEach((i) => { + common.expectsError( + () => session.altsvc('h2=":8000', i), + { + code: 'ERR_HTTP2_ALTSVC_INVALID_ORIGIN', + type: TypeError + } + ); + }); + + // arguments + origin are too long for an ALTSVC frame + common.expectsError( + () => { + session.altsvc('h2=":8000"', + `http://example.${'a'.repeat(17000)}.org:8000`); + }, + { + code: 'ERR_HTTP2_ALTSVC_LENGTH', + type: TypeError + } + ); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const countdown = new Countdown(4, () => { + client.close(); + server.close(); + }); + + client.on('altsvc', common.mustCall((alt, origin, stream) => { + assert.strictEqual(alt, 'h2=":8000"'); + switch (stream) { + case 0: + assert.strictEqual(origin, 'https://example.org:8111'); + break; + case 1: + assert.strictEqual(origin, ''); + break; + default: + assert.fail('should not happen'); + } + countdown.dec(); + }, 4)); + + const req = client.request(); + req.resume(); + req.on('close', common.mustCall()); +})); diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js index 43665029630c12..2f251692d5c412 100644 --- a/test/parallel/test-http2-client-data-end.js +++ b/test/parallel/test-http2-client-data-end.js @@ -5,53 +5,37 @@ 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', common.mustCall((stream, headers, flags) => { - const port = server.address().port; if (headers[':path'] === '/') { - stream.pushStream({ - ':scheme': 'http', - ':path': '/foobar', - ':authority': `localhost:${port}`, - }, (push, headers) => { + stream.pushStream({ ':path': '/foobar' }, (err, push, headers) => { + assert.ifError(err); push.respond({ 'content-type': 'text/html', - ':status': 200, 'x-push-data': 'pushed by server', }); push.write('pushed by server '); - // Sending in next immediate ensures that a second data frame - // will be sent to the client, which will cause the 'data' event - // to fire multiple times. - setImmediate(() => { - push.end('data'); - }); + setImmediate(() => push.end('data')); stream.end('st'); }); } - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); + stream.respond({ 'content-type': 'text/html' }); stream.write('te'); })); server.listen(0, common.mustCall(() => { const port = server.address().port; - const headers = { ':path': '/' }; const client = http2.connect(`http://localhost:${port}`); - const req = client.request(headers); + const req = client.request(); - let expected = 2; - function maybeClose() { - if (--expected === 0) { - server.close(); - client.destroy(); - } - } + const countdown = new Countdown(2, () => { + server.close(); + client.close(); + }); req.on('response', common.mustCall((headers) => { assert.strictEqual(headers[':status'], 200); @@ -70,13 +54,11 @@ server.listen(0, common.mustCall(() => { stream.setEncoding('utf8'); let pushData = ''; - stream.on('data', common.mustCall((d) => { - pushData += d; - }, 2)); + stream.on('data', (d) => pushData += d); stream.on('end', common.mustCall(() => { assert.strictEqual(pushData, 'pushed by server data'); - maybeClose(); })); + stream.on('close', () => countdown.dec()); })); let data = ''; @@ -85,7 +67,6 @@ server.listen(0, common.mustCall(() => { req.on('data', common.mustCallAtLeast((d) => data += d)); req.on('end', common.mustCall(() => { assert.strictEqual(data, 'test'); - maybeClose(); })); - req.end(); + req.on('close', () => countdown.dec()); })); diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js index bb93366247aef7..fab8a4fc24d652 100644 --- a/test/parallel/test-http2-client-destroy.js +++ b/test/parallel/test-http2-client-destroy.js @@ -8,139 +8,115 @@ if (!common.hasCrypto) const assert = require('assert'); const h2 = require('http2'); const { kSocket } = require('internal/http2/util'); +const Countdown = require('../common/countdown'); { const server = h2.createServer(); - server.listen( - 0, - common.mustCall(() => { - const destroyCallbacks = [ - (client) => client.destroy(), - (client) => client[kSocket].destroy() - ]; - - let remaining = destroyCallbacks.length; - - destroyCallbacks.forEach((destroyCallback) => { - const client = h2.connect(`http://localhost:${server.address().port}`); - client.on( - 'connect', - common.mustCall(() => { - const socket = client[kSocket]; - - assert(socket, 'client session has associated socket'); - assert( - !client.destroyed, - 'client has not been destroyed before destroy is called' - ); - assert( - !socket.destroyed, - 'socket has not been destroyed before destroy is called' - ); - - // Ensure that 'close' event is emitted - client.on('close', common.mustCall()); - - destroyCallback(client); - - assert( - !client[kSocket], - 'client.socket undefined after destroy is called' - ); - - // Must must be closed - client.on( - 'close', - common.mustCall(() => { - assert(client.destroyed); - }) - ); - - // socket will close on process.nextTick - socket.on( - 'close', - common.mustCall(() => { - assert(socket.destroyed); - }) - ); - - if (--remaining === 0) { - server.close(); - } - }) + server.listen(0, common.mustCall(() => { + const destroyCallbacks = [ + (client) => client.destroy(), + (client) => client[kSocket].destroy() + ]; + + const countdown = new Countdown(destroyCallbacks.length, () => { + server.close(); + }); + + destroyCallbacks.forEach((destroyCallback) => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.on('connect', common.mustCall(() => { + const socket = client[kSocket]; + + assert(socket, 'client session has associated socket'); + assert( + !client.destroyed, + 'client has not been destroyed before destroy is called' ); - }); - }) - ); + assert( + !socket.destroyed, + 'socket has not been destroyed before destroy is called' + ); + + destroyCallback(client); + + client.on('close', common.mustCall(() => { + assert(client.destroyed); + })); + + countdown.dec(); + })); + }); + })); } // test destroy before client operations { const server = h2.createServer(); - server.listen( - 0, - common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - client.destroy(); - - req.on('response', common.mustNotCall()); - req.resume(); - - const sessionError = { - type: Error, - code: 'ERR_HTTP2_INVALID_SESSION', - message: 'The session has been destroyed' - }; - + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + const socket = client[kSocket]; + socket.on('close', common.mustCall(() => { + assert(socket.destroyed); + })); + + + const req = client.request(); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_CANCEL', + type: Error, + message: 'The pending stream has been canceled' + })); + + client.destroy(); + + req.on('response', common.mustNotCall()); + + const sessionError = { + type: Error, + code: 'ERR_HTTP2_INVALID_SESSION', + message: 'The session has been destroyed' + }; + + common.expectsError(() => client.request(), sessionError); + common.expectsError(() => client.settings({}), sessionError); + client.close(); // should be a non-op at this point + + // Wait for setImmediate call from destroy() to complete + // so that state.destroyed is set to true + setImmediate(() => { common.expectsError(() => client.request(), sessionError); common.expectsError(() => client.settings({}), sessionError); - common.expectsError(() => client.shutdown(), sessionError); - - // Wait for setImmediate call from destroy() to complete - // so that state.destroyed is set to true - setImmediate(() => { - common.expectsError(() => client.request(), sessionError); - common.expectsError(() => client.settings({}), sessionError); - common.expectsError(() => client.shutdown(), sessionError); - }); - - req.on( - 'end', - common.mustCall(() => { - server.close(); - }) - ); - req.end(); - }) - ); + client.close(); // should be a non-op at this point + }); + + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => server.close())); + })); } // test destroy before goaway { const server = h2.createServer(); - server.on( - 'stream', - common.mustCall((stream) => { - stream.on('error', common.mustCall()); - stream.session.shutdown(); - }) - ); - server.listen( - 0, - common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`); + server.on('stream', common.mustCall((stream) => { + stream.session.destroy(); + })); + + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + // On some platforms (e.g. windows), an ECONNRESET may occur at this + // point -- or it may not. Do not make this a mustCall + client.on('error', () => {}); + + client.on('close', () => { + server.close(); + // calling destroy in here should not matter + client.destroy(); + }); - client.on( - 'goaway', - common.mustCall(() => { - // We ought to be able to destroy the client in here without an error - server.close(); - client.destroy(); - }) - ); - - client.request(); - }) - ); + const req = client.request(); + // On some platforms (e.g. windows), an ECONNRESET may occur at this + // point -- or it may not. Do not make this a mustCall + req.on('error', () => {}); + })); } diff --git a/test/parallel/test-http2-client-http1-server.js b/test/parallel/test-http2-client-http1-server.js index 53f7bf42c465e1..616427b3904e16 100644 --- a/test/parallel/test-http2-client-http1-server.js +++ b/test/parallel/test-http2-client-http1-server.js @@ -7,6 +7,7 @@ if (!common.hasCrypto) const http = require('http'); const http2 = require('http2'); +// Creating an http1 server here... const server = http.createServer(common.mustNotCall()); server.listen(0, common.mustCall(() => { @@ -15,13 +16,17 @@ server.listen(0, common.mustCall(() => { const req = client.request(); req.on('close', common.mustCall()); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_ERROR', + type: Error, + message: 'Protocol error' + })); + client.on('error', common.expectsError({ code: 'ERR_HTTP2_ERROR', type: Error, message: 'Protocol error' })); - client.on('close', (...args) => { - server.close(); - }); + client.on('close', common.mustCall(() => server.close())); })); diff --git a/test/parallel/test-http2-client-onconnect-errors.js b/test/parallel/test-http2-client-onconnect-errors.js index 08007753654878..44fe6875602187 100644 --- a/test/parallel/test-http2-client-onconnect-errors.js +++ b/test/parallel/test-http2-client-onconnect-errors.js @@ -1,13 +1,14 @@ 'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + 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 @@ -69,6 +70,8 @@ server.listen(0, common.mustCall(() => runTest(tests.shift()))); function runTest(test) { const client = http2.connect(`http://localhost:${server.address().port}`); + client.on('close', common.mustCall()); + const req = client.request({ ':method': 'POST' }); currentError = test.ngError; @@ -83,15 +86,15 @@ function runTest(test) { if (test.type === 'stream') { client.on('error', errorMustNotCall); req.on('error', errorMustCall); - req.on('error', common.mustCall(() => { - client.destroy(); - })); } else { client.on('error', errorMustCall); - req.on('error', errorMustNotCall); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_CANCEL' + })); } - req.on('end', common.mustCall(() => { + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { client.destroy(); if (!tests.length) { diff --git a/test/parallel/test-http2-client-port-80.js b/test/parallel/test-http2-client-port-80.js index a9d19eb5b9f008..fc82e231f6af8f 100644 --- a/test/parallel/test-http2-client-port-80.js +++ b/test/parallel/test-http2-client-port-80.js @@ -16,4 +16,10 @@ net.connect = common.mustCall((...args) => { }); const client = http2.connect('http://localhost:80'); -client.destroy(); + +// A socket error may or may not occur depending on whether there is something +// currently listening on port 80. Keep this as a non-op and not a mustCall or +// mustNotCall. +client.on('error', () => {}); + +client.close(); diff --git a/test/parallel/test-http2-client-priority-before-connect.js b/test/parallel/test-http2-client-priority-before-connect.js index b062107e4ab7f7..a9615d2cd69f12 100644 --- a/test/parallel/test-http2-client-priority-before-connect.js +++ b/test/parallel/test-http2-client-priority-before-connect.js @@ -8,31 +8,21 @@ 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({ - 'content-type': 'text/html', - ':status': 200 - }); - stream.end('hello world'); -} - -server.listen(0); - -server.on('listening', common.mustCall(() => { +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end('ok'); +})); +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - - const req = client.request({ ':path': '/' }); + const req = client.request(); req.priority({}); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); - req.end(); - })); diff --git a/test/parallel/test-http2-client-promisify-connect.js b/test/parallel/test-http2-client-promisify-connect.js index b66827c1507302..2eb7da3b9cfd85 100644 --- a/test/parallel/test-http2-client-promisify-connect.js +++ b/test/parallel/test-http2-client-promisify-connect.js @@ -15,7 +15,6 @@ server.on('stream', common.mustCall((stream) => { stream.end('ok'); })); server.listen(0, common.mustCall(() => { - const connect = util.promisify(http2.connect); connect(`http://localhost:${server.address().port}`) @@ -28,7 +27,7 @@ server.listen(0, common.mustCall(() => { req.on('data', (chunk) => data += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(data, 'ok'); - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-client-request-options-errors.js b/test/parallel/test-http2-client-request-options-errors.js index 4b146a1bc8c2e4..3ad808cb1fbe23 100644 --- a/test/parallel/test-http2-client-request-options-errors.js +++ b/test/parallel/test-http2-client-request-options-errors.js @@ -33,29 +33,27 @@ server.listen(0, common.mustCall(() => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); - Object.keys(optionsToTest).forEach((option) => { - Object.keys(types).forEach((type) => { - if (type === optionsToTest[option]) { - return; - } - - common.expectsError( - () => client.request({ - ':method': 'CONNECT', - ':authority': `localhost:${port}` - }, { - [option]: types[type] - }), - { - type: TypeError, - code: 'ERR_INVALID_OPT_VALUE', - message: `The value "${String(types[type])}" is invalid ` + - `for option "${option}"` - } - ); + client.on('connect', () => { + Object.keys(optionsToTest).forEach((option) => { + Object.keys(types).forEach((type) => { + if (type === optionsToTest[option]) + return; + + common.expectsError( + () => client.request({ + ':method': 'CONNECT', + ':authority': `localhost:${port}` + }, { + [option]: types[type] + }), { + type: TypeError, + code: 'ERR_INVALID_OPT_VALUE', + message: `The value "${String(types[type])}" is invalid ` + + `for option "${option}"` + }); + }); }); + server.close(); + client.close(); }); - - server.close(); - client.destroy(); })); diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js index eb3a0087d7893c..b0faaa5de2a398 100644 --- a/test/parallel/test-http2-client-rststream-before-connect.js +++ b/test/parallel/test-http2-client-rststream-before-connect.js @@ -8,33 +8,37 @@ const h2 = require('http2'); const server = h2.createServer(); server.on('stream', (stream) => { + stream.on('close', common.mustCall()); stream.respond(); stream.end('ok'); }); -server.listen(0); - -server.on('listening', common.mustCall(() => { - +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - - const req = client.request({ ':path': '/' }); - req.rstStream(0); + const req = client.request(); + req.close(1); + assert.strictEqual(req.closed, true); // make sure that destroy is called req._destroy = common.mustCall(req._destroy.bind(req)); // second call doesn't do anything - assert.doesNotThrow(() => req.rstStream(8)); + assert.doesNotThrow(() => req.close(8)); req.on('close', common.mustCall((code) => { assert.strictEqual(req.destroyed, true); - assert.strictEqual(code, 0); + assert.strictEqual(code, 1); server.close(); - client.destroy(); + client.close(); + })); + + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 1' })); - req.on('response', common.mustNotCall()); + req.on('response', common.mustCall()); req.resume(); req.on('end', common.mustCall()); req.end(); diff --git a/test/parallel/test-http2-client-set-priority.js b/test/parallel/test-http2-client-set-priority.js index f3e1d7afa50d7e..64b7b56dfa2543 100644 --- a/test/parallel/test-http2-client-set-priority.js +++ b/test/parallel/test-http2-client-set-priority.js @@ -10,26 +10,20 @@ const checkWeight = (actual, expect) => { const server = http2.createServer(); server.on('stream', common.mustCall((stream, headers, flags) => { assert.strictEqual(stream.state.weight, expect); - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); + stream.respond(); stream.end('test'); })); server.listen(0, common.mustCall(() => { - const port = server.address().port; - const client = http2.connect(`http://localhost:${port}`); + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({}, { weight: actual }); - const headers = { ':path': '/' }; - const req = client.request(headers, { weight: actual }); - - req.on('data', common.mustCall(() => {})); - req.on('end', common.mustCall(() => { + req.on('data', common.mustCall()); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); - req.end(); })); }; diff --git a/test/parallel/test-http2-client-settings-before-connect.js b/test/parallel/test-http2-client-settings-before-connect.js index 27caa9e601897b..4642bf5220f554 100644 --- a/test/parallel/test-http2-client-settings-before-connect.js +++ b/test/parallel/test-http2-client-settings-before-connect.js @@ -3,62 +3,53 @@ 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({ - 'content-type': 'text/html', - ':status': 200 - }); - stream.end('hello world'); -} - -server.listen(0); - -server.on('listening', common.mustCall(() => { +server.on('stream', common.mustCall((stream, headers, flags) => { + stream.respond(); + stream.end('ok'); +})); +server.on('session', common.mustCall((session) => { + session.on('remoteSettings', common.mustCall(2)); +})); +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - assert.throws(() => client.settings({ headerTableSize: -1 }), - RangeError); - assert.throws(() => client.settings({ headerTableSize: 2 ** 32 }), - RangeError); - assert.throws(() => client.settings({ initialWindowSize: -1 }), - RangeError); - assert.throws(() => client.settings({ initialWindowSize: 2 ** 32 }), - RangeError); - assert.throws(() => client.settings({ maxFrameSize: 1 }), - RangeError); - assert.throws(() => client.settings({ maxFrameSize: 2 ** 24 }), - RangeError); - assert.throws(() => client.settings({ maxConcurrentStreams: -1 }), - RangeError); - assert.throws(() => client.settings({ maxConcurrentStreams: 2 ** 31 }), - RangeError); - assert.throws(() => client.settings({ maxHeaderListSize: -1 }), - RangeError); - assert.throws(() => client.settings({ maxHeaderListSize: 2 ** 32 }), - RangeError); - ['a', 1, 0, null, {}].forEach((i) => { - assert.throws(() => client.settings({ enablePush: i }), TypeError); + [ + ['headerTableSize', -1, RangeError], + ['headerTableSize', 2 ** 32, RangeError], + ['initialWindowSize', -1, RangeError], + ['initialWindowSize', 2 ** 32, RangeError], + ['maxFrameSize', 1, RangeError], + ['maxFrameSize', 2 ** 24, RangeError], + ['maxConcurrentStreams', -1, RangeError], + ['maxConcurrentStreams', 2 ** 31, RangeError], + ['maxHeaderListSize', -1, RangeError], + ['maxHeaderListSize', 2 ** 32, RangeError], + ['enablePush', 'a', TypeError], + ['enablePush', 1, TypeError], + ['enablePush', 0, TypeError], + ['enablePush', null, TypeError], + ['enablePush', {}, TypeError] + ].forEach((i) => { + common.expectsError( + () => client.settings({ [i[0]]: i[1] }), + { + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: i[2] }); }); client.settings({ maxFrameSize: 1234567 }); - const req = client.request({ ':path': '/' }); - + const req = client.request(); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { + req.on('close', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); - req.end(); - })); diff --git a/test/parallel/test-http2-client-shutdown-before-connect.js b/test/parallel/test-http2-client-shutdown-before-connect.js index 4fed0ee3ad0703..bd971ebf7de69c 100644 --- a/test/parallel/test-http2-client-shutdown-before-connect.js +++ b/test/parallel/test-http2-client-shutdown-before-connect.js @@ -10,15 +10,7 @@ const server = h2.createServer(); // we use the lower-level API here server.on('stream', common.mustNotCall()); -server.listen(0); - -server.on('listening', common.mustCall(() => { - +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - - client.shutdown({ graceful: true }, common.mustCall(() => { - server.close(); - client.destroy(); - })); - + client.close(common.mustCall(() => server.close())); })); diff --git a/test/parallel/test-http2-client-socket-destroy.js b/test/parallel/test-http2-client-socket-destroy.js index faf4643b0304e3..3eb7e898edcfc1 100644 --- a/test/parallel/test-http2-client-socket-destroy.js +++ b/test/parallel/test-http2-client-socket-destroy.js @@ -14,38 +14,27 @@ const body = const server = h2.createServer(); // we use the lower-level API here -server.on('stream', common.mustCall(onStream)); - -function onStream(stream) { - // The stream aborted event must have been triggered +server.on('stream', common.mustCall((stream) => { stream.on('aborted', common.mustCall()); - - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); + stream.on('close', common.mustCall()); + stream.respond(); stream.write(body); -} - -server.listen(0); + // purposefully do not end() +})); -server.on('listening', common.mustCall(function() { +server.listen(0, common.mustCall(function() { const client = h2.connect(`http://localhost:${this.address().port}`); - - const req = client.request({ ':path': '/' }); + const req = client.request(); req.on('response', common.mustCall(() => { // send a premature socket close client[kSocket].destroy(); })); - req.on('data', common.mustNotCall()); - req.on('end', common.mustCall(() => { - server.close(); - })); + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => server.close())); // On the client, the close event must call client.on('close', common.mustCall()); - req.end(); - })); diff --git a/test/parallel/test-http2-client-stream-destroy-before-connect.js b/test/parallel/test-http2-client-stream-destroy-before-connect.js index 06afbf3ce8ceb2..a2412b9f1d646a 100644 --- a/test/parallel/test-http2-client-stream-destroy-before-connect.js +++ b/test/parallel/test-http2-client-stream-destroy-before-connect.js @@ -20,36 +20,29 @@ server.on('stream', (stream) => { assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR'); assert.strictEqual(err.message, 'Stream closed with error code 2'); }); - stream.respond({}); + stream.respond(); stream.end(); }); -server.listen(0); - -server.on('listening', common.mustCall(() => { - +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ ':path': '/' }); - const err = new Error('test'); - req.destroy(err); + const req = client.request(); + req.destroy(new Error('test')); - req.on('error', common.mustCall((err) => { - common.expectsError({ - type: Error, - message: 'test' - })(err); + req.on('error', common.expectsError({ + type: Error, + message: 'test' })); req.on('close', common.mustCall((code) => { assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); assert.strictEqual(code, NGHTTP2_INTERNAL_ERROR); server.close(); - client.destroy(); + client.close(); })); req.on('response', common.mustNotCall()); req.resume(); req.on('end', common.mustCall()); - })); diff --git a/test/parallel/test-http2-client-unescaped-path.js b/test/parallel/test-http2-client-unescaped-path.js index adfbd61fe762b4..190f8ce75e8917 100644 --- a/test/parallel/test-http2-client-unescaped-path.js +++ b/test/parallel/test-http2-client-unescaped-path.js @@ -4,6 +4,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); const http2 = require('http2'); +const Countdown = require('../common/countdown'); const server = http2.createServer(); @@ -13,14 +14,12 @@ const count = 32; server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); + client.setMaxListeners(33); - let remaining = count + 1; - function maybeClose() { - if (--remaining === 0) { - server.close(); - client.destroy(); - } - } + const countdown = new Countdown(count + 1, () => { + server.close(); + client.close(); + }); // nghttp2 will catch the bad header value for us. function doTest(i) { @@ -30,7 +29,7 @@ server.listen(0, common.mustCall(() => { type: Error, message: 'Stream closed with error code 1' })); - req.on('close', common.mustCall(maybeClose)); + req.on('close', common.mustCall(() => countdown.dec())); } for (let i = 0; i <= count; i += 1) diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js index 8fb5f369ca4cb7..70a8ff3ced01c6 100644 --- a/test/parallel/test-http2-client-upload.js +++ b/test/parallel/test-http2-client-upload.js @@ -9,6 +9,7 @@ const assert = require('assert'); const http2 = require('http2'); const fs = require('fs'); const fixtures = require('../common/fixtures'); +const Countdown = require('../common/countdown'); const loc = fixtures.path('person.jpg'); let fileData; @@ -34,20 +35,21 @@ fs.readFile(loc, common.mustCall((err, data) => { server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); - let remaining = 2; - function maybeClose() { - if (--remaining === 0) { - server.close(); - client.shutdown(); - } - } + const countdown = new Countdown(2, () => { + server.close(); + client.close(); + }); const req = client.request({ ':method': 'POST' }); req.on('response', common.mustCall()); + req.resume(); - req.on('end', common.mustCall(maybeClose)); + req.on('end', common.mustCall()); + + req.on('finish', () => countdown.dec()); const str = fs.createReadStream(loc); - req.on('finish', common.mustCall(maybeClose)); + str.on('end', common.mustCall()); + str.on('close', () => countdown.dec()); str.pipe(req); })); })); diff --git a/test/parallel/test-http2-client-write-before-connect.js b/test/parallel/test-http2-client-write-before-connect.js index 26674dcad369e3..6588d7dccd139d 100644 --- a/test/parallel/test-http2-client-write-before-connect.js +++ b/test/parallel/test-http2-client-write-before-connect.js @@ -8,47 +8,30 @@ const h2 = require('http2'); const server = h2.createServer(); -const { - HTTP2_HEADER_PATH, - HTTP2_HEADER_METHOD, - HTTP2_METHOD_POST -} = h2.constants; - // we use the lower-level API here -server.on('stream', common.mustCall(onStream)); - -function onStream(stream, headers, flags) { +server.on('stream', common.mustCall((stream, headers, flags) => { let data = ''; stream.setEncoding('utf8'); stream.on('data', (chunk) => data += chunk); stream.on('end', common.mustCall(() => { assert.strictEqual(data, 'some data more data'); })); - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); - stream.end('hello world'); -} - -server.listen(0); - -server.on('listening', common.mustCall(() => { + stream.respond(); + stream.end('ok'); +})); +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ - [HTTP2_HEADER_PATH]: '/', - [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); + const req = client.request({ ':method': 'POST' }); req.write('some data '); - req.write('more data'); + req.end('more data'); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); - req.end(); - })); diff --git a/test/parallel/test-http2-compat-errors.js b/test/parallel/test-http2-compat-errors.js index 5774d1a922bd52..c84318bad68e35 100644 --- a/test/parallel/test-http2-compat-errors.js +++ b/test/parallel/test-http2-compat-errors.js @@ -4,9 +4,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const assert = require('assert'); const h2 = require('http2'); -const { Http2Stream } = require('internal/http2/core'); // Errors should not be reported both in Http2ServerRequest // and Http2ServerResponse @@ -14,6 +12,7 @@ const { Http2Stream } = require('internal/http2/core'); let expected = null; const server = h2.createServer(common.mustCall(function(req, res) { + res.stream.on('error', common.mustCall()); req.on('error', common.mustNotCall()); res.on('error', common.mustNotCall()); req.on('aborted', common.mustCall()); @@ -26,27 +25,12 @@ const server = h2.createServer(common.mustCall(function(req, res) { server.close(); })); -server.on('streamError', common.mustCall(function(err, stream) { - assert.strictEqual(err, expected); - assert.strictEqual(stream instanceof Http2Stream, true); -})); - server.listen(0, common.mustCall(function() { - const port = server.address().port; - - const url = `http://localhost:${port}`; - const client = h2.connect(url, common.mustCall(function() { - const headers = { - ':path': '/foobar', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}`, - }; - const request = client.request(headers); - request.on('data', common.mustCall(function(chunk) { - // cause an error on the server side + const url = `http://localhost:${server.address().port}`; + const client = h2.connect(url, common.mustCall(() => { + const request = client.request(); + request.on('data', common.mustCall((chunk) => { client.destroy(); })); - request.end(); })); })); diff --git a/test/parallel/test-http2-compat-expect-continue-check.js b/test/parallel/test-http2-compat-expect-continue-check.js index 800df1c432944f..6aded8b52935c1 100644 --- a/test/parallel/test-http2-compat-expect-continue-check.js +++ b/test/parallel/test-http2-compat-expect-continue-check.js @@ -12,74 +12,47 @@ const testResBody = 'other stuff!\n'; // through server receiving it, triggering 'checkContinue' custom handler, // writing the rest of the request to finally the client receiving to. -function handler(req, res) { - console.error('Server sent full response'); +const server = http2.createServer( + common.mustNotCall('Full request received before 100 Continue') +); - res.writeHead(200, { - 'content-type': 'text/plain', - 'abcd': '1' - }); +server.on('checkContinue', common.mustCall((req, res) => { + res.writeContinue(); + res.writeHead(200, {}); res.end(testResBody); // should simply return false if already too late to write assert.strictEqual(res.writeContinue(), false); res.on('finish', common.mustCall( () => process.nextTick(() => assert.strictEqual(res.writeContinue(), false)) )); -} - -const server = http2.createServer( - common.mustNotCall('Full request received before 100 Continue') -); - -server.on('checkContinue', common.mustCall((req, res) => { - console.error('Server received Expect: 100-continue'); - - res.writeContinue(); - - // timeout so that we allow the client to receive continue first - setTimeout( - common.mustCall(() => handler(req, res)), - common.platformTimeout(100) - ); })); -server.listen(0); - -server.on('listening', common.mustCall(() => { +server.listen(0, common.mustCall(() => { let body = ''; - const port = server.address().port; - const client = http2.connect(`http://localhost:${port}`); + const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request({ ':method': 'POST', - ':path': '/world', expect: '100-continue' }); - console.error('Client sent request'); let gotContinue = false; req.on('continue', common.mustCall(() => { - console.error('Client received 100-continue'); gotContinue = true; })); req.on('response', common.mustCall((headers) => { - console.error('Client received response headers'); - assert.strictEqual(gotContinue, true); assert.strictEqual(headers[':status'], 200); - assert.strictEqual(headers['abcd'], '1'); + req.end(); })); req.setEncoding('utf-8'); req.on('data', common.mustCall((chunk) => { body += chunk; })); req.on('end', common.mustCall(() => { - console.error('Client received full response'); - assert.strictEqual(body, testResBody); - - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-compat-expect-continue.js b/test/parallel/test-http2-compat-expect-continue.js index 6f08e813ef385a..42fa80ae4e8620 100644 --- a/test/parallel/test-http2-compat-expect-continue.js +++ b/test/parallel/test-http2-compat-expect-continue.js @@ -17,8 +17,6 @@ const server = http2.createServer(); let sentResponse = false; server.on('request', common.mustCall((req, res) => { - console.error('Server sent full response'); - res.end(testResBody); sentResponse = true; })); @@ -28,38 +26,29 @@ server.listen(0); server.on('listening', common.mustCall(() => { let body = ''; - const port = server.address().port; - const client = http2.connect(`http://localhost:${port}`); + const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request({ ':method': 'POST', - ':path': '/world', expect: '100-continue' }); - console.error('Client sent request'); let gotContinue = false; req.on('continue', common.mustCall(() => { - console.error('Client received 100-continue'); gotContinue = true; })); req.on('response', common.mustCall((headers) => { - console.error('Client received response headers'); - assert.strictEqual(gotContinue, true); assert.strictEqual(sentResponse, true); assert.strictEqual(headers[':status'], 200); + req.end(); })); req.setEncoding('utf8'); req.on('data', common.mustCall((chunk) => { body += chunk; })); - req.on('end', common.mustCall(() => { - console.error('Client received full response'); - assert.strictEqual(body, testResBody); - - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-compat-expect-handling.js b/test/parallel/test-http2-compat-expect-handling.js index 0a5de368c6cfff..f36032c972fc45 100644 --- a/test/parallel/test-http2-compat-expect-handling.js +++ b/test/parallel/test-http2-compat-expect-handling.js @@ -39,7 +39,7 @@ function nextTest(testsToRun) { })); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); nextTest(testsToRun - 1); })); } diff --git a/test/parallel/test-http2-compat-method-connect.js b/test/parallel/test-http2-compat-method-connect.js index 1f43b3891b24ed..21ad23e92ba65b 100644 --- a/test/parallel/test-http2-compat-method-connect.js +++ b/test/parallel/test-http2-compat-method-connect.js @@ -33,7 +33,7 @@ function testMethodConnect(testsToRun) { })); req.resume(); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); testMethodConnect(testsToRun - 1); })); req.end(); diff --git a/test/parallel/test-http2-compat-serverrequest-end.js b/test/parallel/test-http2-compat-serverrequest-end.js index b6bfd04089a103..d34372118582db 100644 --- a/test/parallel/test-http2-compat-serverrequest-end.js +++ b/test/parallel/test-http2-compat-serverrequest-end.js @@ -31,18 +31,11 @@ server.listen(0, common.mustCall(function() { })); const url = `http://localhost:${port}`; - const client = h2.connect(url, common.mustCall(function() { - const headers = { - ':path': '/foobar', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - const request = client.request(headers); + const client = h2.connect(url, common.mustCall(() => { + const request = client.request(); request.resume(); - request.on('end', common.mustCall(function() { - client.destroy(); + request.on('end', common.mustCall(() => { + client.close(); })); - request.end(); })); })); diff --git a/test/parallel/test-http2-compat-serverrequest-headers.js b/test/parallel/test-http2-compat-serverrequest-headers.js index 58cc52c64f6c91..5843104c019189 100644 --- a/test/parallel/test-http2-compat-serverrequest-headers.js +++ b/test/parallel/test-http2-compat-serverrequest-headers.js @@ -79,7 +79,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverrequest-pause.js b/test/parallel/test-http2-compat-serverrequest-pause.js index f8494bb0ddee39..62a23997c75bd8 100644 --- a/test/parallel/test-http2-compat-serverrequest-pause.js +++ b/test/parallel/test-http2-compat-serverrequest-pause.js @@ -46,7 +46,7 @@ server.listen(0, common.mustCall(() => { request.resume(); request.end(testStr); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-compat-serverrequest-pipe.js b/test/parallel/test-http2-compat-serverrequest-pipe.js index 04c8cfe546f329..becc62c6621e7f 100644 --- a/test/parallel/test-http2-compat-serverrequest-pipe.js +++ b/test/parallel/test-http2-compat-serverrequest-pipe.js @@ -35,7 +35,7 @@ server.listen(0, common.mustCall(() => { function maybeClose() { if (--remaining === 0) { server.close(); - client.destroy(); + client.close(); } } diff --git a/test/parallel/test-http2-compat-serverrequest-settimeout.js b/test/parallel/test-http2-compat-serverrequest-settimeout.js index 460eb576bfd4f6..f7189161802301 100644 --- a/test/parallel/test-http2-compat-serverrequest-settimeout.js +++ b/test/parallel/test-http2-compat-serverrequest-settimeout.js @@ -12,7 +12,6 @@ const server = http2.createServer(); server.on('request', (req, res) => { req.setTimeout(msecs, common.mustCall(() => { res.end(); - req.setTimeout(msecs, common.mustNotCall()); })); res.on('finish', common.mustCall(() => { req.setTimeout(msecs, common.mustNotCall()); @@ -35,7 +34,7 @@ server.listen(0, common.mustCall(() => { ':authority': `localhost:${port}` }); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); })); req.resume(); req.end(); diff --git a/test/parallel/test-http2-compat-serverrequest-trailers.js b/test/parallel/test-http2-compat-serverrequest-trailers.js index b4d90281918d9e..285178cab66816 100644 --- a/test/parallel/test-http2-compat-serverrequest-trailers.js +++ b/test/parallel/test-http2-compat-serverrequest-trailers.js @@ -62,7 +62,7 @@ server.listen(0, common.mustCall(function() { request.resume(); request.on('end', common.mustCall(function() { server.close(); - client.destroy(); + client.close(); })); request.write('test\n'); request.end('test'); diff --git a/test/parallel/test-http2-compat-serverrequest.js b/test/parallel/test-http2-compat-serverrequest.js index edcd7a8f8cdea4..d92da61d943cb7 100644 --- a/test/parallel/test-http2-compat-serverrequest.js +++ b/test/parallel/test-http2-compat-serverrequest.js @@ -46,7 +46,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-close.js b/test/parallel/test-http2-compat-serverresponse-close.js index 35e39b9670868e..0ff6bd3a83f600 100644 --- a/test/parallel/test-http2-compat-serverresponse-close.js +++ b/test/parallel/test-http2-compat-serverresponse-close.js @@ -16,26 +16,17 @@ const server = h2.createServer(common.mustCall((req, res) => { req.on('close', common.mustCall()); res.on('close', common.mustCall()); + req.on('error', common.mustNotCall()); })); server.listen(0); -server.on('listening', function() { - const port = server.address().port; - - const url = `http://localhost:${port}`; - const client = h2.connect(url, common.mustCall(function() { - const headers = { - ':path': '/foobar', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}`, - }; - const request = client.request(headers); +server.on('listening', () => { + const url = `http://localhost:${server.address().port}`; + const client = h2.connect(url, common.mustCall(() => { + const request = client.request(); request.on('data', common.mustCall(function(chunk) { - // cause an error on the server side client.destroy(); server.close(); })); - request.end(); })); }); diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js index bd9d5c1399792a..b168c3563c32e2 100644 --- a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js +++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js @@ -43,7 +43,7 @@ const server = h2.createServer((request, response) => { ':path': '/pushed', ':method': 'GET' }, common.mustCall((error) => { - assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_CLOSED'); + assert.strictEqual(error.code, 'ERR_HTTP2_INVALID_STREAM'); })); }); })); @@ -61,7 +61,7 @@ server.listen(0, common.mustCall(() => { let remaining = 2; function maybeClose() { if (--remaining === 0) { - client.destroy(); + client.close(); server.close(); } } diff --git a/test/parallel/test-http2-compat-serverresponse-destroy.js b/test/parallel/test-http2-compat-serverresponse-destroy.js index 77e761b6227702..54214737840061 100644 --- a/test/parallel/test-http2-compat-serverresponse-destroy.js +++ b/test/parallel/test-http2-compat-serverresponse-destroy.js @@ -5,6 +5,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const http2 = require('http2'); +const Countdown = require('../common/countdown'); // Check that destroying the Http2ServerResponse stream produces // the expected result, including the ability to throw an error @@ -30,63 +31,54 @@ const server = http2.createServer(common.mustCall((req, res) => { if (req.url !== '/') { nextError = errors.shift(); } + res.destroy(nextError); }, 3)); -server.on( - 'streamError', - common.mustCall((err) => assert.strictEqual(err, nextError), 2) -); - server.listen(0, common.mustCall(() => { - const port = server.address().port; - const client = http2.connect(`http://localhost:${port}`); - const req = client.request({ - ':path': '/', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }); + const client = http2.connect(`http://localhost:${server.address().port}`); - req.on('response', common.mustNotCall()); - req.on('error', common.mustNotCall()); - req.on('end', common.mustCall()); + const countdown = new Countdown(3, () => { + server.close(); + client.close(); + }); - req.resume(); - req.end(); + { + const req = client.request(); + req.on('response', common.mustNotCall()); + req.on('error', common.mustNotCall()); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => countdown.dec())); + req.resume(); + } - const req2 = client.request({ - ':path': '/error', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }); + { + const req = client.request({ ':path': '/error' }); - req2.on('response', common.mustNotCall()); - req2.on('error', common.mustNotCall()); - req2.on('end', common.mustCall()); + req.on('response', common.mustNotCall()); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + })); + req.on('close', common.mustCall(() => countdown.dec())); - req2.resume(); - req2.end(); + req.resume(); + req.on('end', common.mustCall()); + } - const req3 = client.request({ - ':path': '/error', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }); + { + const req = client.request({ ':path': '/error' }); - req3.on('response', common.mustNotCall()); - req3.on('error', common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - type: Error, - message: 'Stream closed with error code 2' - })); - req3.on('end', common.mustCall(() => { - server.close(); - client.destroy(); - })); + req.on('response', common.mustNotCall()); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + })); + req.on('close', common.mustCall(() => countdown.dec())); - req3.resume(); - req3.end(); + req.resume(); + req.on('end', common.mustCall()); + } })); diff --git a/test/parallel/test-http2-compat-serverresponse-drain.js b/test/parallel/test-http2-compat-serverresponse-drain.js index e2465cfa00d1f6..7ccbb1f4d21209 100644 --- a/test/parallel/test-http2-compat-serverresponse-drain.js +++ b/test/parallel/test-http2-compat-serverresponse-drain.js @@ -37,7 +37,7 @@ server.listen(0, common.mustCall(() => { request.on('end', common.mustCall(function() { assert.strictEqual(data, testString.repeat(2)); - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-compat-serverresponse-end.js b/test/parallel/test-http2-compat-serverresponse-end.js index 366d52321554fe..0e846a5948e3cc 100644 --- a/test/parallel/test-http2-compat-serverresponse-end.js +++ b/test/parallel/test-http2-compat-serverresponse-end.js @@ -52,7 +52,7 @@ const { request.on('data', (chunk) => (data += chunk)); request.on('end', mustCall(() => { strictEqual(data, 'end'); - client.destroy(); + client.close(); })); request.end(); request.resume(); @@ -83,7 +83,7 @@ const { request.on('data', (chunk) => (data += chunk)); request.on('end', mustCall(() => { strictEqual(data, 'test\uD83D\uDE00'); - client.destroy(); + client.close(); })); request.end(); request.resume(); @@ -110,7 +110,7 @@ const { }; const request = client.request(headers); request.on('data', mustNotCall()); - request.on('end', mustCall(() => client.destroy())); + request.on('end', mustCall(() => client.close())); request.end(); request.resume(); })); @@ -143,7 +143,7 @@ const { })); request.on('data', mustNotCall()); request.on('end', mustCall(() => { - client.destroy(); + client.close(); server.close(); })); request.end(); @@ -172,7 +172,7 @@ const { const request = client.request(headers); request.on('data', mustNotCall()); request.on('end', mustCall(() => { - client.destroy(); + client.close(); server.close(); })); request.end(); @@ -208,7 +208,7 @@ const { })); request.on('data', mustNotCall()); request.on('end', mustCall(() => { - client.destroy(); + client.close(); server.close(); })); request.end(); @@ -243,7 +243,7 @@ const { })); request.on('data', mustNotCall()); request.on('end', mustCall(() => { - client.destroy(); + client.close(); server.close(); })); request.end(); @@ -283,7 +283,7 @@ const { })); request.on('data', mustNotCall()); request.on('end', mustCall(() => { - client.destroy(); + client.close(); server.close(); })); request.end(); @@ -315,7 +315,7 @@ const { })); request.on('data', mustNotCall()); request.on('end', mustCall(() => { - client.destroy(); + client.close(); server.close(); })); request.end(); diff --git a/test/parallel/test-http2-compat-serverresponse-finished.js b/test/parallel/test-http2-compat-serverresponse-finished.js index b816b922202dd6..ceaa6eb5c3cf2c 100644 --- a/test/parallel/test-http2-compat-serverresponse-finished.js +++ b/test/parallel/test-http2-compat-serverresponse-finished.js @@ -39,7 +39,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-flushheaders.js b/test/parallel/test-http2-compat-serverresponse-flushheaders.js index 68d4789f69be53..d155b07863d26c 100644 --- a/test/parallel/test-http2-compat-serverresponse-flushheaders.js +++ b/test/parallel/test-http2-compat-serverresponse-flushheaders.js @@ -51,7 +51,7 @@ server.listen(0, common.mustCall(function() { serverResponse.end(); }, 1)); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js b/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js index fb1e369f786bf0..99e3ccc948184e 100644 --- a/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js +++ b/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js @@ -13,8 +13,6 @@ const server = h2.createServer(); server.listen(0, common.mustCall(function() { const port = server.address().port; server.once('request', common.mustCall(function(request, response) { - response.destroy(); - response.on('finish', common.mustCall(() => { assert.strictEqual(response.headersSent, false); assert.doesNotThrow(() => response.setHeader('test', 'value')); @@ -27,6 +25,8 @@ server.listen(0, common.mustCall(function() { server.close(); }); })); + + response.destroy(); })); const url = `http://localhost:${port}`; @@ -39,7 +39,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-headers.js b/test/parallel/test-http2-compat-serverresponse-headers.js index 6da74307e0d340..7b5313b8e7b037 100644 --- a/test/parallel/test-http2-compat-serverresponse-headers.js +++ b/test/parallel/test-http2-compat-serverresponse-headers.js @@ -179,7 +179,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-settimeout.js b/test/parallel/test-http2-compat-serverresponse-settimeout.js index 6d06d07f7dc0ab..bb09633727ccf7 100644 --- a/test/parallel/test-http2-compat-serverresponse-settimeout.js +++ b/test/parallel/test-http2-compat-serverresponse-settimeout.js @@ -12,7 +12,6 @@ const server = http2.createServer(); server.on('request', (req, res) => { res.setTimeout(msecs, common.mustCall(() => { res.end(); - res.setTimeout(msecs, common.mustNotCall()); })); res.on('finish', common.mustCall(() => { res.setTimeout(msecs, common.mustNotCall()); @@ -35,7 +34,7 @@ server.listen(0, common.mustCall(() => { ':authority': `localhost:${port}` }); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); })); req.resume(); req.end(); diff --git a/test/parallel/test-http2-compat-serverresponse-statuscode.js b/test/parallel/test-http2-compat-serverresponse-statuscode.js index 6a573da88f9858..ecb6da5df68c11 100644 --- a/test/parallel/test-http2-compat-serverresponse-statuscode.js +++ b/test/parallel/test-http2-compat-serverresponse-statuscode.js @@ -69,7 +69,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js b/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js index 45a876d674313b..87e172402899f2 100644 --- a/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js @@ -42,7 +42,7 @@ server.listen(0, common.mustCall(function() { assert.strictEqual(headers[':status'], 200); }, 1)); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js index 21a5b6ea4e2820..8a083cf3ba1638 100644 --- a/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js @@ -41,7 +41,7 @@ server.listen(0, common.mustCall(function() { assert.strictEqual(headers[':status'], 200); }, 1)); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage.js b/test/parallel/test-http2-compat-serverresponse-statusmessage.js index 841bafe724a7a8..dee916d1aeef54 100644 --- a/test/parallel/test-http2-compat-serverresponse-statusmessage.js +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage.js @@ -45,7 +45,7 @@ server.listen(0, common.mustCall(function() { assert.strictEqual(headers['foo-bar'], 'abc123'); }, 1)); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-serverresponse-trailers.js b/test/parallel/test-http2-compat-serverresponse-trailers.js index 7332f9e8d0b63d..66ad8843fa33b9 100644 --- a/test/parallel/test-http2-compat-serverresponse-trailers.js +++ b/test/parallel/test-http2-compat-serverresponse-trailers.js @@ -68,7 +68,7 @@ server.listen(0, common.mustCall(() => { })); request.resume(); request.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-compat-serverresponse-write-no-cb.js b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js index 58a4ca053d222c..a62bb1b0ac78f1 100644 --- a/test/parallel/test-http2-compat-serverresponse-write-no-cb.js +++ b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js @@ -6,44 +6,33 @@ const { mustCall, hasCrypto, skip } = require('../common'); if (!hasCrypto) skip('missing crypto'); -const { throws } = require('assert'); const { createServer, connect } = require('http2'); // Http2ServerResponse.write does not imply there is a callback -const expectedError = expectsError({ - code: 'ERR_HTTP2_STREAM_CLOSED', - message: 'The stream is already closed' -}, 2); - { const server = createServer(); server.listen(0, mustCall(() => { const port = server.address().port; const url = `http://localhost:${port}`; const client = connect(url, mustCall(() => { - const headers = { - ':path': '/', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - const request = client.request(headers); - request.end(); + const request = client.request(); request.resume(); + request.on('end', mustCall()); + request.on('close', mustCall(() => { + client.close(); + })); })); server.once('request', mustCall((request, response) => { client.destroy(); response.stream.session.on('close', mustCall(() => { response.on('error', mustNotCall()); - throws( + expectsError( () => { response.write('muahaha'); }, - expectsError({ - code: 'ERR_HTTP2_STREAM_CLOSED', - type: Error, - message: 'The stream is already closed' - }) + { + code: 'ERR_HTTP2_INVALID_STREAM' + } ); server.close(); })); @@ -57,21 +46,21 @@ const expectedError = expectsError({ const port = server.address().port; const url = `http://localhost:${port}`; const client = connect(url, mustCall(() => { - const headers = { - ':path': '/', - ':method': 'get', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - const request = client.request(headers); - request.end(); + const request = client.request(); request.resume(); + request.on('end', mustCall()); + request.on('close', mustCall(() => client.close())); })); server.once('request', mustCall((request, response) => { client.destroy(); response.stream.session.on('close', mustCall(() => { - response.write('muahaha', mustCall(expectedError)); + expectsError( + () => response.write('muahaha'), + { + code: 'ERR_HTTP2_INVALID_STREAM' + } + ); server.close(); })); })); @@ -84,20 +73,20 @@ const expectedError = expectsError({ const port = server.address().port; const url = `http://localhost:${port}`; const client = connect(url, mustCall(() => { - const headers = { - ':path': '/', - ':method': 'get', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - const request = client.request(headers); - request.end(); + const request = client.request(); request.resume(); + request.on('end', mustCall()); + request.on('close', mustCall(() => client.close())); })); server.once('request', mustCall((request, response) => { response.stream.session.on('close', mustCall(() => { - response.write('muahaha', 'utf8', mustCall(expectedError)); + expectsError( + () => response.write('muahaha', 'utf8'), + { + code: 'ERR_HTTP2_INVALID_STREAM' + } + ); server.close(); })); client.destroy(); diff --git a/test/parallel/test-http2-compat-serverresponse-writehead.js b/test/parallel/test-http2-compat-serverresponse-writehead.js index 704f199ca27e99..5fd787e100350c 100644 --- a/test/parallel/test-http2-compat-serverresponse-writehead.js +++ b/test/parallel/test-http2-compat-serverresponse-writehead.js @@ -23,7 +23,7 @@ server.listen(0, common.mustCall(function() { server.close(); process.nextTick(common.mustCall(() => { common.expectsError(() => { response.writeHead(300); }, { - code: 'ERR_HTTP2_STREAM_CLOSED' + code: 'ERR_HTTP2_INVALID_STREAM' }); })); })); @@ -44,7 +44,7 @@ server.listen(0, common.mustCall(function() { assert.strictEqual(headers[':status'], 418); }, 1)); request.on('end', common.mustCall(function() { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-compat-socket-set.js b/test/parallel/test-http2-compat-socket-set.js index ff53d998f4d207..05beb09d548e91 100644 --- a/test/parallel/test-http2-compat-socket-set.js +++ b/test/parallel/test-http2-compat-socket-set.js @@ -98,7 +98,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); request.end(); diff --git a/test/parallel/test-http2-compat-socket.js b/test/parallel/test-http2-compat-socket.js index d30b2f2a11613b..80c8b1d30d10b3 100644 --- a/test/parallel/test-http2-compat-socket.js +++ b/test/parallel/test-http2-compat-socket.js @@ -81,7 +81,7 @@ server.listen(0, common.mustCall(function() { }; const request = client.request(headers); request.on('end', common.mustCall(() => { - client.destroy(); + client.close(); })); request.end(); request.resume(); diff --git a/test/parallel/test-http2-connect-method.js b/test/parallel/test-http2-connect-method.js index 4d443d5c217421..0ddbc60433cf85 100644 --- a/test/parallel/test-http2-connect-method.js +++ b/test/parallel/test-http2-connect-method.js @@ -13,7 +13,8 @@ const { HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_SCHEME, HTTP2_HEADER_PATH, - NGHTTP2_CONNECT_ERROR + NGHTTP2_CONNECT_ERROR, + NGHTTP2_REFUSED_STREAM } = http2.constants; const server = net.createServer(common.mustCall((socket) => { @@ -34,7 +35,7 @@ server.listen(0, common.mustCall(() => { const proxy = http2.createServer(); proxy.on('stream', common.mustCall((stream, headers) => { if (headers[HTTP2_HEADER_METHOD] !== 'CONNECT') { - stream.rstWithRefused(); + stream.close(NGHTTP2_REFUSED_STREAM); return; } const auth = new URL(`tcp://${headers[HTTP2_HEADER_AUTHORITY]}`); @@ -47,7 +48,7 @@ server.listen(0, common.mustCall(() => { }); socket.on('close', common.mustCall()); socket.on('error', (error) => { - stream.rstStream(NGHTTP2_CONNECT_ERROR); + stream.close(NGHTTP2_CONNECT_ERROR); }); })); @@ -99,7 +100,7 @@ server.listen(0, common.mustCall(() => { req.on('data', (chunk) => data += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(data, 'hello'); - client.destroy(); + client.close(); proxy.close(); server.close(); })); diff --git a/test/parallel/test-http2-connect.js b/test/parallel/test-http2-connect.js index e5a4e429907090..894c51fe3d9330 100644 --- a/test/parallel/test-http2-connect.js +++ b/test/parallel/test-http2-connect.js @@ -20,7 +20,7 @@ const { createServer, connect } = require('http2'); for (const client of clients) { client.once('connect', mustCall((headers) => { - client.destroy(); + client.close(); clients.delete(client); if (clients.size === 0) { server.close(); @@ -33,7 +33,11 @@ const { createServer, connect } = require('http2'); // check for https as protocol { const authority = 'https://localhost'; - doesNotThrow(() => connect(authority)); + doesNotThrow(() => { + // A socket error may or may not be reported, keep this as a non-op + // instead of a mustCall or mustNotCall + connect(authority).on('error', () => {}); + }); } // check for error for an invalid protocol (not http or https) diff --git a/test/parallel/test-http2-cookies.js b/test/parallel/test-http2-cookies.js index 48b08b6367b4b3..cf763915389287 100644 --- a/test/parallel/test-http2-cookies.js +++ b/test/parallel/test-http2-cookies.js @@ -54,7 +54,7 @@ server.on('listening', common.mustCall(() => { req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end(); diff --git a/test/parallel/test-http2-create-client-connect.js b/test/parallel/test-http2-create-client-connect.js index cd7d8b4fc8c3f9..fa9711fe1b28a7 100644 --- a/test/parallel/test-http2-create-client-connect.js +++ b/test/parallel/test-http2-create-client-connect.js @@ -30,7 +30,7 @@ const URL = url.URL; () => setImmediate(() => server.close())); const maybeClose = common.mustCall((client) => { - client.destroy(); + client.close(); serverClose.dec(); }, items.length); @@ -42,7 +42,7 @@ const URL = url.URL; // Will fail because protocol does not match the server. h2.connect({ port: port, protocol: 'https:' }) - .on('socketError', common.mustCall(() => serverClose.dec())); + .on('error', common.mustCall(() => serverClose.dec())); })); } @@ -55,10 +55,8 @@ const URL = url.URL; }; const server = h2.createSecureServer(options); - server.listen(0); - - server.on('listening', common.mustCall(function() { - const port = this.address().port; + server.listen(0, common.mustCall(() => { + const port = server.address().port; const opts = { rejectUnauthorized: false }; @@ -74,7 +72,7 @@ const URL = url.URL; () => setImmediate(() => server.close())); const maybeClose = common.mustCall((client) => { - client.destroy(); + client.close(); serverClose.dec(); }, items.length); diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js index 811ef772d5903a..6120a58602065d 100644 --- a/test/parallel/test-http2-create-client-secure-session.js +++ b/test/parallel/test-http2-create-client-secure-session.js @@ -19,6 +19,14 @@ function loadKey(keyname) { function onStream(stream, headers) { const socket = stream.session[kSocket]; + + assert(stream.session.encrypted); + assert(stream.session.alpnProtocol, 'h2'); + const originSet = stream.session.originSet; + assert(Array.isArray(originSet)); + assert.strictEqual(originSet[0], + `https://${socket.servername}:${socket.remotePort}`); + assert(headers[':authority'].startsWith(socket.servername)); stream.respond({ 'content-type': 'application/json' }); stream.end(JSON.stringify({ @@ -39,6 +47,17 @@ function verifySecureSession(key, cert, ca, opts) { assert.strictEqual(client.socket.listenerCount('secureConnect'), 1); const req = client.request(); + client.on('connect', common.mustCall(() => { + assert(client.encrypted); + assert.strictEqual(client.alpnProtocol, 'h2'); + const originSet = client.originSet; + assert(Array.isArray(originSet)); + assert.strictEqual(originSet.length, 1); + assert.strictEqual( + originSet[0], + `https://${opts.servername || 'localhost'}:${server.address().port}`); + })); + req.on('response', common.mustCall((headers) => { assert.strictEqual(headers[':status'], 200); assert.strictEqual(headers['content-type'], 'application/json'); diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js index 149b5164231a21..b5be6bc8581452 100644 --- a/test/parallel/test-http2-create-client-session.js +++ b/test/parallel/test-http2-create-client-session.js @@ -5,6 +5,8 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const h2 = require('http2'); +const Countdown = require('../common/countdown'); + const body = '

this is some data

'; @@ -23,21 +25,32 @@ function onStream(stream, headers, flags) { 'content-type': 'text/html', ':status': 200 }); - stream.end(body); + stream.write(body.slice(0, 20)); + stream.end(body.slice(20)); } server.listen(0); -let expected = count; +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + client.setMaxListeners(101); -server.on('listening', common.mustCall(function() { + client.on('goaway', console.log); - const client = h2.connect(`http://localhost:${this.address().port}`); + client.on('connect', common.mustCall(() => { + assert(!client.encrypted); + assert(!client.originSet); + assert.strictEqual(client.alpnProtocol, 'h2c'); + })); - const headers = { ':path': '/' }; + const countdown = new Countdown(count, () => { + client.close(); + server.close(); + }); for (let n = 0; n < count; n++) { - const req = client.request(headers); + const req = client.request(); req.on('response', common.mustCall(function(headers) { assert.strictEqual(headers[':status'], 200, 'status code is set'); @@ -51,12 +64,7 @@ server.on('listening', common.mustCall(function() { req.on('data', (d) => data += d); req.on('end', common.mustCall(() => { assert.strictEqual(body, data); - if (--expected === 0) { - server.close(); - client.destroy(); - } })); - req.end(); + req.on('close', common.mustCall(() => countdown.dec())); } - })); diff --git a/test/parallel/test-http2-createsecureserver-nooptions.js b/test/parallel/test-http2-createsecureserver-nooptions.js index 8ff7f22e3ffcf6..71764f5783e404 100644 --- a/test/parallel/test-http2-createsecureserver-nooptions.js +++ b/test/parallel/test-http2-createsecureserver-nooptions.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) const http2 = require('http2'); -const invalidOptions = [() => {}, 1, 'test', null, undefined]; +const invalidOptions = [() => {}, 1, 'test', null]; const invalidArgTypeError = { type: TypeError, code: 'ERR_INVALID_ARG_TYPE', @@ -14,9 +14,9 @@ const invalidArgTypeError = { }; // Error if options are not passed to createSecureServer -invalidOptions.forEach((invalidOption) => +invalidOptions.forEach((invalidOption) => { common.expectsError( () => http2.createSecureServer(invalidOption), invalidArgTypeError - ) -); + ); +}); diff --git a/test/parallel/test-http2-createwritereq.js b/test/parallel/test-http2-createwritereq.js index ca394a5d425470..1d2b31676284d0 100644 --- a/test/parallel/test-http2-createwritereq.js +++ b/test/parallel/test-http2-createwritereq.js @@ -1,5 +1,7 @@ 'use strict'; +// Flags: --expose-gc + const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -54,7 +56,7 @@ server.listen(0, common.mustCall(function() { req.resume(); req.on('end', common.mustCall(function() { - client.destroy(); + client.close(); testsFinished++; if (testsFinished === testsToRun) { @@ -62,6 +64,15 @@ server.listen(0, common.mustCall(function() { } })); + // Ref: https://github.com/nodejs/node/issues/17840 + const origDestroy = req.destroy; + req.destroy = function(...args) { + // Schedule a garbage collection event at the end of the current + // MakeCallback() run. + process.nextTick(global.gc); + return origDestroy.call(this, ...args); + }; + req.end(); }); })); diff --git a/test/parallel/test-http2-date-header.js b/test/parallel/test-http2-date-header.js index ab0654e64cbcd7..2b63e1b7899a2e 100644 --- a/test/parallel/test-http2-date-header.js +++ b/test/parallel/test-http2-date-header.js @@ -24,6 +24,6 @@ server.listen(0, common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); })); diff --git a/test/parallel/test-http2-dont-lose-data.js b/test/parallel/test-http2-dont-lose-data.js new file mode 100644 index 00000000000000..eb85277b7b124c --- /dev/null +++ b/test/parallel/test-http2-dont-lose-data.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', (s) => { + assert(s.pushAllowed); + + s.pushStream({ ':path': '/file' }, common.mustCall((err, pushStream) => { + assert.ifError(err); + pushStream.respond(); + pushStream.end('a push stream'); + })); + + s.respond(); + s.end('hello world'); +}); + +server.listen(0, () => { + server.unref(); + + const url = `http://localhost:${server.address().port}`; + + const client = http2.connect(url); + const req = client.request(); + + let pushStream; + + client.on('stream', common.mustCall((s, headers) => { + assert.strictEqual(headers[':path'], '/file'); + pushStream = s; + })); + + req.on('response', common.mustCall((headers) => { + let pushData = ''; + pushStream.setEncoding('utf8'); + pushStream.on('data', (d) => pushData += d); + pushStream.on('end', common.mustCall(() => { + assert.strictEqual(pushData, 'a push stream'); + + // removing the setImmediate causes the test to pass + setImmediate(function() { + let data = ''; + req.setEncoding('utf8'); + req.on('data', (d) => data += d); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'hello world'); + client.close(); + })); + }); + })); + })); +}); diff --git a/test/parallel/test-http2-dont-override.js b/test/parallel/test-http2-dont-override.js index cc60a2fe802a82..b45713deb3ca60 100644 --- a/test/parallel/test-http2-dont-override.js +++ b/test/parallel/test-http2-dont-override.js @@ -44,6 +44,6 @@ server.listen(0, common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); })); diff --git a/test/parallel/test-http2-generic-streams-sendfile.js b/test/parallel/test-http2-generic-streams-sendfile.js index 1054574a8b1ca2..b752b0fdcb815a 100644 --- a/test/parallel/test-http2-generic-streams-sendfile.js +++ b/test/parallel/test-http2-generic-streams-sendfile.js @@ -20,7 +20,7 @@ const makeDuplexPair = require('../common/duplexpair'); createConnection: common.mustCall(() => clientSide) }); - const req = client.request({ ':path': '/' }); + const req = client.request(); req.on('response', common.mustCall((headers) => { assert.strictEqual(headers[':status'], 200); @@ -28,9 +28,7 @@ const makeDuplexPair = require('../common/duplexpair'); req.setEncoding('utf8'); let data = ''; - req.on('data', (chunk) => { - data += chunk; - }); + req.on('data', (chunk) => data += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(data, fs.readFileSync(__filename, 'utf8')); clientSide.destroy(); diff --git a/test/parallel/test-http2-goaway-opaquedata.js b/test/parallel/test-http2-goaway-opaquedata.js index d8895a82edf464..3f1fb4d7954414 100644 --- a/test/parallel/test-http2-goaway-opaquedata.js +++ b/test/parallel/test-http2-goaway-opaquedata.js @@ -10,32 +10,23 @@ const server = http2.createServer(); const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); server.on('stream', common.mustCall((stream) => { - stream.session.shutdown({ - errorCode: 1, - opaqueData: data - }); + stream.session.goaway(0, 0, data); + stream.respond(); stream.end(); - stream.on('error', common.mustCall(common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - type: Error, - message: 'Stream closed with error code 7' - }))); })); server.listen(0, () => { const client = http2.connect(`http://localhost:${server.address().port}`); - client.on('goaway', common.mustCall((code, lastStreamID, buf) => { - assert.deepStrictEqual(code, 1); - assert.deepStrictEqual(lastStreamID, 0); + client.once('goaway', common.mustCall((code, lastStreamID, buf) => { + assert.deepStrictEqual(code, 0); + assert.deepStrictEqual(lastStreamID, 1); assert.deepStrictEqual(data, buf); - // Call shutdown() here so that emitGoaway calls destroy() - client.shutdown(); server.close(); })); - const req = client.request({ ':path': '/' }); + const req = client.request(); req.resume(); req.on('end', common.mustCall()); + req.on('close', common.mustCall()); req.end(); - }); diff --git a/test/parallel/test-http2-head-request.js b/test/parallel/test-http2-head-request.js index d15665624ac192..a56abf3c9006f3 100644 --- a/test/parallel/test-http2-head-request.js +++ b/test/parallel/test-http2-head-request.js @@ -54,6 +54,6 @@ server.listen(0, () => { req.on('data', common.mustNotCall()); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); }); diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js index 04e9ca480c9099..01b694e586dd49 100644 --- a/test/parallel/test-http2-https-fallback.js +++ b/test/parallel/test-http2-https-fallback.js @@ -52,7 +52,7 @@ function onSession(session) { strictEqual(alpnProtocol, 'h2'); strictEqual(httpVersion, '2.0'); - session.destroy(); + session.close(); this.cleanup(); })); request.end(); diff --git a/test/parallel/test-http2-info-headers-errors.js b/test/parallel/test-http2-info-headers-errors.js index b671bece4f76fc..1df76334558529 100644 --- a/test/parallel/test-http2-info-headers-errors.js +++ b/test/parallel/test-http2-info-headers-errors.js @@ -48,10 +48,6 @@ server.on('stream', common.mustCall((stream, headers) => { 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); @@ -63,24 +59,21 @@ server.on('stream', common.mustCall((stream, headers) => { 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; req.resume(); req.end(); - req.on('end', common.mustCall(() => { - client.destroy(); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + })); + + req.on('close', common.mustCall(() => { + client.close(); if (!tests.length) { server.close(); diff --git a/test/parallel/test-http2-info-headers.js b/test/parallel/test-http2-info-headers.js index 332c688429c37e..2313040de12a97 100644 --- a/test/parallel/test-http2-info-headers.js +++ b/test/parallel/test-http2-info-headers.js @@ -88,7 +88,7 @@ server.on('listening', common.mustCall(() => { req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end(); diff --git a/test/parallel/test-http2-invalidargtypes-errors.js b/test/parallel/test-http2-invalidargtypes-errors.js index 3471e46fdf4ca4..ff189a2977559f 100644 --- a/test/parallel/test-http2-invalidargtypes-errors.js +++ b/test/parallel/test-http2-invalidargtypes-errors.js @@ -7,29 +7,25 @@ const http2 = require('http2'); const server = http2.createServer(); -server.on( - 'stream', - common.mustCall((stream) => { - const invalidArgTypeError = (param, type) => ({ - type: TypeError, +server.on('stream', common.mustCall((stream) => { + common.expectsError( + () => stream.close('string'), + { code: 'ERR_INVALID_ARG_TYPE', - message: `The "${param}" argument must be of type ${type}` - }); - common.expectsError( - () => stream.rstStream('string'), - invalidArgTypeError('code', 'number') - ); - stream.session.destroy(); - }) -); + type: TypeError, + message: 'The "code" argument must be of type number' + } + ); + stream.respond(); + stream.end('ok'); +})); -server.listen( - 0, - common.mustCall(() => { - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - req.resume(); - req.on('end', common.mustCall(() => server.close())); - req.end(); - }) -); +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.resume(); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/parallel/test-http2-max-concurrent-streams.js b/test/parallel/test-http2-max-concurrent-streams.js index a65ac90c535b03..ffc04e98f134b2 100644 --- a/test/parallel/test-http2-max-concurrent-streams.js +++ b/test/parallel/test-http2-max-concurrent-streams.js @@ -5,64 +5,52 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const h2 = require('http2'); - -const { - HTTP2_HEADER_METHOD, - HTTP2_HEADER_STATUS, - HTTP2_HEADER_PATH, - HTTP2_METHOD_POST -} = h2.constants; +const Countdown = require('../common/countdown'); // Only allow one stream to be open at a time const server = h2.createServer({ settings: { maxConcurrentStreams: 1 } }); // The stream handler must be called only once server.on('stream', common.mustCall((stream) => { - stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + stream.respond(); stream.end('hello world'); })); -server.listen(0); - -server.on('listening', common.mustCall(() => { +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - let reqs = 2; - function onEnd() { - if (--reqs === 0) { - server.close(); - client.destroy(); - } - } + const countdown = new Countdown(2, () => { + server.close(); + client.close(); + }); client.on('remoteSettings', common.mustCall((settings) => { assert.strictEqual(settings.maxConcurrentStreams, 1); })); // This one should go through with no problems - const req1 = client.request({ - [HTTP2_HEADER_PATH]: '/', - [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST - }); - req1.on('aborted', common.mustNotCall()); - req1.on('response', common.mustCall()); - req1.resume(); - req1.on('end', onEnd); - req1.end(); - - // This one should be aborted - const req2 = client.request({ - [HTTP2_HEADER_PATH]: '/', - [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST - }); - req2.on('aborted', common.mustCall()); - req2.on('response', common.mustNotCall()); - req2.resume(); - req2.on('end', onEnd); - req2.on('error', common.mustCall(common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - type: Error, - message: 'Stream closed with error code 7' - }))); + { + const req = client.request({ ':method': 'POST' }); + req.on('aborted', common.mustNotCall()); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => countdown.dec())); + req.end(); + } + { + // This one should be aborted + const req = client.request({ ':method': 'POST' }); + req.on('aborted', common.mustCall()); + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => countdown.dec())); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 7' + })); + } })); diff --git a/test/parallel/test-http2-methods.js b/test/parallel/test-http2-methods.js index 36f64f13abcf86..a291bdf00800d5 100644 --- a/test/parallel/test-http2-methods.js +++ b/test/parallel/test-http2-methods.js @@ -41,7 +41,7 @@ server.on('listening', common.mustCall(() => { req.on('end', common.mustCall(() => { if (--expected === 0) { server.close(); - client.destroy(); + client.close(); } })); req.end(); diff --git a/test/parallel/test-http2-misbehaving-flow-control-paused.js b/test/parallel/test-http2-misbehaving-flow-control-paused.js index ee799b1d5a27d3..0b7299d5ac80a8 100644 --- a/test/parallel/test-http2-misbehaving-flow-control-paused.js +++ b/test/parallel/test-http2-misbehaving-flow-control-paused.js @@ -56,32 +56,24 @@ let client; const server = h2.createServer({ settings: { initialWindowSize: 36 } }); server.on('stream', (stream) => { - - // Not reading causes the flow control window to get backed up. stream.pause(); - - stream.on('error', common.mustCall((err) => { - common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - type: Error, - message: 'Stream closed with error code 3' - })(err); + stream.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 3' + })); + stream.on('close', common.mustCall(() => { server.close(); client.destroy(); })); - stream.on('end', common.mustNotCall()); - stream.respond(); stream.end('ok'); }); server.listen(0, () => { client = net.connect(server.address().port, () => { - client.on('error', console.log); - client.write(preamble); - client.write(data); client.write(data); client.write(data); diff --git a/test/parallel/test-http2-misbehaving-flow-control.js b/test/parallel/test-http2-misbehaving-flow-control.js index 010e07741316b6..8a0b411b8de65c 100644 --- a/test/parallel/test-http2-misbehaving-flow-control.js +++ b/test/parallel/test-http2-misbehaving-flow-control.js @@ -29,6 +29,21 @@ const preamble = Buffer.from([ ]); const data = Buffer.from([ + 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, + 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, + 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, + 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, + 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a @@ -51,30 +66,23 @@ const data = Buffer.from([ let client; const server = h2.createServer({ settings: { initialWindowSize: 18 } }); server.on('stream', (stream) => { - - stream.resume(); - - stream.on('error', common.mustCall((err) => { - common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - type: Error, - message: 'Stream closed with error code 3' - })(err); + stream.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 3' + })); + stream.on('close', common.mustCall(() => { server.close(); client.destroy(); })); - + stream.resume(); stream.respond(); stream.end('ok'); }); server.listen(0, () => { client = net.connect(server.address().port, () => { - client.on('error', console.log); - client.write(preamble); - - client.write(data); client.write(data); client.write(data); }); diff --git a/test/parallel/test-http2-misbehaving-multiplex.js b/test/parallel/test-http2-misbehaving-multiplex.js new file mode 100644 index 00000000000000..7d5a7a2f552d49 --- /dev/null +++ b/test/parallel/test-http2-misbehaving-multiplex.js @@ -0,0 +1,59 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const h2 = require('http2'); +const net = require('net'); +const h2test = require('../common/http2'); +let client; + +const server = h2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end('ok'); +}, 2)); +server.on('session', common.mustCall((session) => { + session.on('error', common.expectsError({ + code: 'ERR_HTTP2_ERROR', + type: Error, + message: 'Stream was already closed or invalid' + })); +})); + +const settings = new h2test.SettingsFrame(); +const settingsAck = new h2test.SettingsFrame(true); +const head1 = new h2test.HeadersFrame(1, h2test.kFakeRequestHeaders, 0, true); +const head2 = new h2test.HeadersFrame(3, h2test.kFakeRequestHeaders, 0, true); +const head3 = new h2test.HeadersFrame(1, h2test.kFakeRequestHeaders, 0, true); +const head4 = new h2test.HeadersFrame(5, h2test.kFakeRequestHeaders, 0, true); + +server.listen(0, () => { + client = net.connect(server.address().port, () => { + client.write(h2test.kClientMagic, () => { + client.write(settings.data, () => { + client.write(settingsAck.data); + // This will make it ok. + client.write(head1.data, () => { + // This will make it ok. + client.write(head2.data, () => { + // This will cause an error to occur because the client is + // attempting to reuse an already closed stream. This must + // cause the server session to be torn down. + client.write(head3.data, () => { + // This won't ever make it to the server + client.write(head4.data); + }); + }); + }); + }); + }); + }); + + // An error may or may not be emitted on the client side, we don't care + // either way if it is, but we don't want to die if it is. + client.on('error', () => {}); + client.on('close', common.mustCall(() => server.close())); +}); diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js index 1f501598c1c421..b47462b14fd0e1 100644 --- a/test/parallel/test-http2-misused-pseudoheaders.js +++ b/test/parallel/test-http2-misused-pseudoheaders.js @@ -7,11 +7,7 @@ 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) { - +server.on('stream', common.mustCall((stream) => { [ ':path', ':authority', @@ -24,10 +20,7 @@ function onStream(stream, headers, flags) { }); }); - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }, { + stream.respond({}, { getTrailers: common.mustCall((trailers) => { trailers[':status'] = 'bar'; }) @@ -38,22 +31,24 @@ function onStream(stream, headers, flags) { })); stream.end('hello world'); -} - -server.listen(0); +})); -server.on('listening', common.mustCall(() => { +server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); - const req = client.request({ ':path': '/' }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + })); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); - req.end(); - })); diff --git a/test/parallel/test-http2-multi-content-length.js b/test/parallel/test-http2-multi-content-length.js index d0f0094d2408aa..4d18356f127da0 100644 --- a/test/parallel/test-http2-multi-content-length.js +++ b/test/parallel/test-http2-multi-content-length.js @@ -4,6 +4,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); const http2 = require('http2'); +const Countdown = require('../common/countdown'); const server = http2.createServer(); @@ -15,29 +16,25 @@ server.on('stream', common.mustCall((stream) => { server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); - let remaining = 3; - function maybeClose() { - if (--remaining === 0) { - server.close(); - client.destroy(); - } - } + const countdown = new Countdown(2, () => { + server.close(); + client.close(); + }); - { - // Request 1 will fail because there are two content-length header values - const req = client.request({ - ':method': 'POST', - 'content-length': 1, - 'Content-Length': 2 - }); - req.on('error', common.expectsError({ + // Request 1 will fail because there are two content-length header values + common.expectsError( + () => { + client.request({ + ':method': 'POST', + 'content-length': 1, + 'Content-Length': 2 + }); + }, { code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', type: Error, message: 'Header field "content-length" must have only a single value' - })); - req.on('error', common.mustCall(maybeClose)); - req.end('a'); - } + } + ); { // Request 2 will succeed @@ -46,7 +43,8 @@ server.listen(0, common.mustCall(() => { 'content-length': 1 }); req.resume(); - req.on('end', common.mustCall(maybeClose)); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => countdown.dec())); req.end('a'); } @@ -55,7 +53,8 @@ server.listen(0, common.mustCall(() => { // header to be set for non-payload bearing requests... const req = client.request({ 'content-length': 1 }); req.resume(); - req.on('end', common.mustCall(maybeClose)); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => countdown.dec())); req.on('error', common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR', type: Error, diff --git a/test/parallel/test-http2-multiheaders-raw.js b/test/parallel/test-http2-multiheaders-raw.js index c06bf23bff3071..50486450d5aeb7 100644 --- a/test/parallel/test-http2-multiheaders-raw.js +++ b/test/parallel/test-http2-multiheaders-raw.js @@ -44,6 +44,6 @@ server.listen(0, common.mustCall(() => { const req = client.request(src); req.on('close', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); })); diff --git a/test/parallel/test-http2-multiheaders.js b/test/parallel/test-http2-multiheaders.js index 5e477104091cb1..9bf8f76d22e60e 100644 --- a/test/parallel/test-http2-multiheaders.js +++ b/test/parallel/test-http2-multiheaders.js @@ -56,6 +56,6 @@ server.listen(0, common.mustCall(() => { req.on('response', common.mustCall(checkHeaders)); req.on('close', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); })); diff --git a/test/parallel/test-http2-multiplex.js b/test/parallel/test-http2-multiplex.js index c818a28572eca7..1778bced5f92f4 100644 --- a/test/parallel/test-http2-multiplex.js +++ b/test/parallel/test-http2-multiplex.js @@ -8,6 +8,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const http2 = require('http2'); +const Countdown = require('../common/countdown'); const server = http2.createServer(); @@ -20,15 +21,12 @@ server.on('stream', common.mustCall((stream) => { server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); + client.setMaxListeners(100); - let remaining = count; - - function maybeClose() { - if (--remaining === 0) { - server.close(); - client.destroy(); - } - } + const countdown = new Countdown(count, () => { + server.close(); + client.close(); + }); function doRequest() { const req = client.request({ ':method': 'POST ' }); @@ -38,8 +36,8 @@ server.listen(0, common.mustCall(() => { req.on('data', (chunk) => data += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(data, 'abcdefghij'); - maybeClose(); })); + req.on('close', common.mustCall(() => countdown.dec())); let n = 0; function writeChunk() { diff --git a/test/parallel/test-http2-no-more-streams.js b/test/parallel/test-http2-no-more-streams.js index 6f4169756c0b4a..dd06a709f23023 100644 --- a/test/parallel/test-http2-no-more-streams.js +++ b/test/parallel/test-http2-no-more-streams.js @@ -25,7 +25,7 @@ server.listen(0, common.mustCall(() => { const countdown = new Countdown(2, common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); { diff --git a/test/parallel/test-http2-options-max-headers-block-length.js b/test/parallel/test-http2-options-max-headers-block-length.js index 402803dd33a73a..a728c28c6576d4 100644 --- a/test/parallel/test-http2-options-max-headers-block-length.js +++ b/test/parallel/test-http2-options-max-headers-block-length.js @@ -10,9 +10,7 @@ const server = h2.createServer(); // we use the lower-level API here server.on('stream', common.mustNotCall()); -server.listen(0); - -server.on('listening', common.mustCall(() => { +server.listen(0, common.mustCall(() => { // Setting the maxSendHeaderBlockLength, then attempting to send a // headers block that is too big should cause a 'frameError' to @@ -24,13 +22,13 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`, options); - const req = client.request({ ':path': '/' }); - + const req = client.request(); req.on('response', common.mustNotCall()); req.resume(); - req.on('end', common.mustCall(() => { - client.destroy(); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); })); req.on('frameError', common.mustCall((type, code) => { @@ -42,33 +40,4 @@ server.on('listening', common.mustCall(() => { type: Error, message: 'Stream closed with error code 7' })); - - req.end(); - - // if no frameError listener, should emit 'error' with - // code ERR_HTTP2_FRAME_ERROR - const req2 = client.request({ ':path': '/' }); - - req2.on('response', common.mustNotCall()); - - req2.resume(); - req2.on('end', common.mustCall(() => { - server.close(); - client.destroy(); - })); - - req2.once('error', common.mustCall((err) => { - common.expectsError({ - code: 'ERR_HTTP2_FRAME_ERROR', - type: Error - })(err); - req2.on('error', common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - type: Error, - message: 'Stream closed with error code 7' - })); - })); - - req2.end(); - })); diff --git a/test/parallel/test-http2-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js index d54ca6a7886b3c..994a8817451686 100644 --- a/test/parallel/test-http2-options-max-reserved-streams.js +++ b/test/parallel/test-http2-options-max-reserved-streams.js @@ -5,20 +5,24 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const h2 = require('http2'); +const Countdown = require('../common/countdown'); const server = h2.createServer(); +let client; + +const countdown = new Countdown(3, () => { + server.close(); + client.close(); +}); // we use the lower-level API here server.on('stream', common.mustCall((stream) => { - stream.respond({ ':status': 200 }); - // The first pushStream will complete as normal stream.pushStream({ - ':scheme': 'http', ':path': '/foobar', - ':authority': `localhost:${server.address().port}`, - }, common.mustCall((pushedStream) => { - pushedStream.respond({ ':status': 200 }); + }, common.mustCall((err, pushedStream) => { + assert.ifError(err); + pushedStream.respond(); pushedStream.end(); pushedStream.on('aborted', common.mustNotCall()); })); @@ -27,52 +31,41 @@ server.on('stream', common.mustCall((stream) => { // will reject it due to the maxReservedRemoteStreams option // being set to only 1 stream.pushStream({ - ':scheme': 'http', ':path': '/foobar', - ':authority': `localhost:${server.address().port}`, - }, common.mustCall((pushedStream) => { - pushedStream.respond({ ':status': 200 }); + }, common.mustCall((err, pushedStream) => { + assert.ifError(err); + pushedStream.respond(); pushedStream.on('aborted', common.mustCall()); pushedStream.on('error', common.mustNotCall()); - pushedStream.on('close', - common.mustCall((code) => assert.strictEqual(code, 8))); + pushedStream.on('close', common.mustCall((code) => { + assert.strictEqual(code, 8); + countdown.dec(); + })); })); + stream.respond(); stream.end('hello world'); })); server.listen(0); server.on('listening', common.mustCall(() => { + client = h2.connect(`http://localhost:${server.address().port}`, + { maxReservedRemoteStreams: 1 }); - const options = { - maxReservedRemoteStreams: 1 - }; - - const client = h2.connect(`http://localhost:${server.address().port}`, - options); - - let remaining = 2; - function maybeClose() { - if (--remaining === 0) { - server.close(); - client.destroy(); - } - } - - const req = client.request({ ':path': '/' }); + const req = client.request(); // Because maxReservedRemoteStream is 1, the stream event // must only be emitted once, even tho the server sends // two push streams. client.on('stream', common.mustCall((stream) => { stream.resume(); + stream.on('push', common.mustCall()); stream.on('end', common.mustCall()); - stream.on('close', common.mustCall(maybeClose)); + stream.on('close', common.mustCall(() => countdown.dec())); })); req.on('response', common.mustCall()); - req.resume(); req.on('end', common.mustCall()); - req.on('close', common.mustCall(maybeClose)); + req.on('close', common.mustCall(() => countdown.dec())); })); diff --git a/test/parallel/test-http2-padding-aligned.js b/test/parallel/test-http2-padding-aligned.js new file mode 100644 index 00000000000000..183eaef7389360 --- /dev/null +++ b/test/parallel/test-http2-padding-aligned.js @@ -0,0 +1,68 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const { PADDING_STRATEGY_ALIGNED } = http2.constants; +const makeDuplexPair = require('../common/duplexpair'); + +{ + const testData = '

Hello World.

'; + const server = http2.createServer({ + paddingStrategy: PADDING_STRATEGY_ALIGNED + }); + server.on('stream', common.mustCall((stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end(testData); + })); + + const { clientSide, serverSide } = makeDuplexPair(); + + // The lengths of the expected writes... note that this is highly + // sensitive to how the internals are implemented. + const serverLengths = [24, 9, 9, 32]; + const clientLengths = [9, 9, 48, 9, 1, 21, 1, 16]; + + // Adjust for the 24-byte preamble and two 9-byte settings frames, and + // the result must be equally divisible by 8 + assert.strictEqual( + (serverLengths.reduce((i, n) => i + n) - 24 - 9 - 9) % 8, 0); + + // Adjust for two 9-byte settings frames, and the result must be equally + // divisible by 8 + assert.strictEqual( + (clientLengths.reduce((i, n) => i + n) - 9 - 9) % 8, 0); + + serverSide.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk.length, serverLengths.shift()); + }, serverLengths.length)); + clientSide.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk.length, clientLengths.shift()); + }, clientLengths.length)); + + server.emit('connection', serverSide); + + const client = http2.connect('http://localhost:80', { + paddingStrategy: PADDING_STRATEGY_ALIGNED, + createConnection: common.mustCall(() => clientSide) + }); + + const req = client.request({ ':path': '/a' }); + + req.on('response', common.mustCall()); + + req.setEncoding('utf8'); + req.on('data', common.mustCall((data) => { + assert.strictEqual(data, testData); + })); + req.on('close', common.mustCall(() => { + clientSide.destroy(); + clientSide.end(); + })); + req.end(); +} diff --git a/test/parallel/test-http2-padding-callback.js b/test/parallel/test-http2-padding-callback.js index af547ad498da1b..6d6a6b27221b07 100644 --- a/test/parallel/test-http2-padding-callback.js +++ b/test/parallel/test-http2-padding-callback.js @@ -45,7 +45,7 @@ server.on('listening', common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end(); })); diff --git a/test/parallel/test-http2-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js new file mode 100644 index 00000000000000..f2ef29cec25e06 --- /dev/null +++ b/test/parallel/test-http2-perf_hooks.js @@ -0,0 +1,95 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const { PerformanceObserver } = require('perf_hooks'); + +const obs = new PerformanceObserver((items) => { + const entry = items.getEntries()[0]; + assert.strictEqual(entry.entryType, 'http2'); + assert.strictEqual(typeof entry.startTime, 'number'); + assert.strictEqual(typeof entry.duration, 'number'); + switch (entry.name) { + case 'Http2Session': + assert.strictEqual(typeof entry.pingRTT, 'number'); + assert.strictEqual(typeof entry.streamAverageDuration, 'number'); + assert.strictEqual(typeof entry.streamCount, 'number'); + assert.strictEqual(typeof entry.framesReceived, 'number'); + switch (entry.type) { + case 'server': + assert.strictEqual(entry.streamCount, 1); + assert.strictEqual(entry.framesReceived, 5); + break; + case 'client': + assert.strictEqual(entry.streamCount, 1); + assert.strictEqual(entry.framesReceived, 8); + break; + default: + assert.fail('invalid Http2Session type'); + } + break; + case 'Http2Stream': + assert.strictEqual(typeof entry.timeToFirstByte, 'number'); + assert.strictEqual(typeof entry.timeToFirstHeader, 'number'); + break; + default: + assert.fail('invalid entry name'); + } +}); +obs.observe({ entryTypes: ['http2'] }); + +const body = + '

this is some data

'; + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + assert.strictEqual(headers[':scheme'], 'http'); + assert.ok(headers[':authority']); + assert.strictEqual(headers[':method'], 'GET'); + assert.strictEqual(flags, 5); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write(body.slice(0, 20)); + stream.end(body.slice(20)); +} + +server.on('session', common.mustCall((session) => { + session.ping(common.mustCall()); +})); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + client.on('connect', common.mustCall(() => { + client.ping(common.mustCall()); + })); + + const req = client.request(); + + req.on('response', common.mustCall()); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (d) => data += d); + req.on('end', common.mustCall(() => { + assert.strictEqual(body, data); + })); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); + +})); diff --git a/test/parallel/test-http2-ping-unsolicited-ack.js b/test/parallel/test-http2-ping-unsolicited-ack.js new file mode 100644 index 00000000000000..5a3a261cb098b1 --- /dev/null +++ b/test/parallel/test-http2-ping-unsolicited-ack.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const net = require('net'); +const http2util = require('../common/http2'); + +// Test that ping flooding causes the session to be torn down + +const kSettings = new http2util.SettingsFrame(); +const kPingAck = new http2util.PingFrame(true); + +const server = http2.createServer(); + +server.on('stream', common.mustNotCall()); +server.on('session', common.mustCall((session) => { + session.on('error', common.expectsError({ + code: 'ERR_HTTP2_ERROR', + message: 'Protocol error' + })); + session.on('close', common.mustCall(() => server.close())); +})); + +server.listen(0, common.mustCall(() => { + const client = net.connect(server.address().port); + + client.on('connect', common.mustCall(() => { + client.write(http2util.kClientMagic, () => { + client.write(kSettings.data); + // Send an unsolicited ping ack + client.write(kPingAck.data); + }); + })); + + // An error event may or may not be emitted, depending on operating system + // and timing. We do not really care if one is emitted here or not, as the + // error on the server side is what we are testing for. Do not make this + // a common.mustCall() and there's no need to check the error details. + client.on('error', () => {}); +})); diff --git a/test/parallel/test-http2-ping.js b/test/parallel/test-http2-ping.js index 4892d67b4d738d..32fb8926e4716c 100644 --- a/test/parallel/test-http2-ping.js +++ b/test/parallel/test-http2-ping.js @@ -80,7 +80,7 @@ server.listen(0, common.mustCall(() => { const req = client.request(); req.resume(); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-pipe.js b/test/parallel/test-http2-pipe.js index 8b446f4f88118b..891fc6e292b909 100644 --- a/test/parallel/test-http2-pipe.js +++ b/test/parallel/test-http2-pipe.js @@ -8,7 +8,6 @@ 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 @@ -20,28 +19,28 @@ const server = http2.createServer(); server.on('stream', common.mustCall((stream) => { const dest = stream.pipe(fs.createWriteStream(fn)); - dest.on('finish', common.mustCall(() => { - assert.strictEqual(fs.readFileSync(loc).length, fs.readFileSync(fn).length); - fs.unlinkSync(fn); - stream.respond(); - stream.end(); - })); + + dest.on('finish', () => { + assert.strictEqual(fs.readFileSync(loc).length, + fs.readFileSync(fn).length); + }); + stream.respond(); + stream.end(); })); server.listen(0, common.mustCall(() => { - const port = server.address().port; - const client = http2.connect(`http://localhost:${port}`); - - const countdown = new Countdown(2, common.mustCall(() => { - server.close(); - client.destroy(); - })); + const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request({ ':method': 'POST' }); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => countdown.dec())); + + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); + const str = fs.createReadStream(loc); - str.on('end', common.mustCall(() => countdown.dec())); + str.on('end', common.mustCall()); str.pipe(req); })); diff --git a/test/parallel/test-http2-priority-cycle-.js b/test/parallel/test-http2-priority-cycle-.js new file mode 100644 index 00000000000000..af0d66d8343cbf --- /dev/null +++ b/test/parallel/test-http2-priority-cycle-.js @@ -0,0 +1,69 @@ +'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(); +const largeBuffer = Buffer.alloc(1e4); + +// Verify that a dependency cycle may exist, but that it doesn't crash anything + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + setImmediate(() => { + stream.end(largeBuffer); + }); +}, 3)); +server.on('session', common.mustCall((session) => { + session.on('priority', (id, parent, weight, exclusive) => { + assert.strictEqual(weight, 16); + assert.strictEqual(exclusive, false); + switch (id) { + case 1: + assert.strictEqual(parent, 5); + break; + case 3: + assert.strictEqual(parent, 1); + break; + case 5: + assert.strictEqual(parent, 3); + break; + default: + assert.fail('should not happen'); + } + }); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const countdown = new Countdown(3, () => { + client.close(); + server.close(); + }); + + { + const req = client.request(); + req.priority({ parent: 5 }); + req.resume(); + req.on('close', () => countdown.dec()); + } + + { + const req = client.request(); + req.priority({ parent: 1 }); + req.resume(); + req.on('close', () => countdown.dec()); + } + + { + const req = client.request(); + req.priority({ parent: 3 }); + req.resume(); + req.on('close', () => countdown.dec()); + } +})); diff --git a/test/parallel/test-http2-priority-event.js b/test/parallel/test-http2-priority-event.js index b0704902d31101..fe04ffb342d70d 100644 --- a/test/parallel/test-http2-priority-event.js +++ b/test/parallel/test-http2-priority-event.js @@ -54,7 +54,7 @@ server.on('listening', common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js index dcc05357faedad..2a48456c9394a4 100644 --- a/test/parallel/test-http2-respond-errors.js +++ b/test/parallel/test-http2-respond-errors.js @@ -74,13 +74,18 @@ function runTest(test) { const client = http2.connect(url); const req = client.request(headers); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + })); currentError = test; req.resume(); req.end(); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); if (!tests.length) { server.close(); diff --git a/test/parallel/test-http2-respond-file-204.js b/test/parallel/test-http2-respond-file-204.js index 4be2d42c779a5e..b0ba634e67df69 100644 --- a/test/parallel/test-http2-respond-file-204.js +++ b/test/parallel/test-http2-respond-file-204.js @@ -34,7 +34,7 @@ server.listen(0, () => { req.on('response', common.mustCall()); req.on('data', common.mustNotCall()); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-304.js b/test/parallel/test-http2-respond-file-304.js index e6e0842c7f9448..536c48c624e73c 100644 --- a/test/parallel/test-http2-respond-file-304.js +++ b/test/parallel/test-http2-respond-file-304.js @@ -38,7 +38,7 @@ server.listen(0, () => { req.on('data', common.mustNotCall()); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-404.js b/test/parallel/test-http2-respond-file-404.js index ba62f384485bc0..60bc21f185dd5c 100644 --- a/test/parallel/test-http2-respond-file-404.js +++ b/test/parallel/test-http2-respond-file-404.js @@ -40,7 +40,7 @@ server.listen(0, () => { })); req.on('data', common.mustNotCall()); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-compat.js b/test/parallel/test-http2-respond-file-compat.js index 0f6e3199d68ab2..0205f2d0d85aaf 100644 --- a/test/parallel/test-http2-respond-file-compat.js +++ b/test/parallel/test-http2-respond-file-compat.js @@ -16,7 +16,7 @@ server.listen(0, () => { const req = client.request(); req.on('response', common.mustCall()); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-error-dir.js b/test/parallel/test-http2-respond-file-error-dir.js index 18a9540451f865..6818616227df89 100644 --- a/test/parallel/test-http2-respond-file-error-dir.js +++ b/test/parallel/test-http2-respond-file-error-dir.js @@ -6,14 +6,10 @@ if (!common.hasCrypto) const http2 = require('http2'); const assert = require('assert'); -const { - HTTP2_HEADER_CONTENT_TYPE -} = http2.constants; - const server = http2.createServer(); server.on('stream', (stream) => { stream.respondWithFile(process.cwd(), { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }, { onError(err) { common.expectsError({ @@ -38,7 +34,7 @@ server.listen(0, () => { })); req.on('data', common.mustNotCall()); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-errors.js b/test/parallel/test-http2-respond-file-errors.js index c2c749873c82ac..83d3900bc5c288 100644 --- a/test/parallel/test-http2-respond-file-errors.js +++ b/test/parallel/test-http2-respond-file-errors.js @@ -6,11 +6,6 @@ if (!common.hasCrypto) const fixtures = require('../common/fixtures'); const http2 = require('http2'); -const { - HTTP2_HEADER_CONTENT_TYPE, - HTTP2_HEADER_METHOD -} = http2.constants; - const optionsWithTypeError = { offset: 'number', length: 'number', @@ -33,6 +28,7 @@ const fname = fixtures.path('elipses.txt'); const server = http2.createServer(); server.on('stream', common.mustCall((stream) => { + // Check for all possible TypeError triggers on options Object.keys(optionsWithTypeError).forEach((option) => { Object.keys(types).forEach((type) => { @@ -42,7 +38,7 @@ server.on('stream', common.mustCall((stream) => { common.expectsError( () => stream.respondWithFile(fname, { - [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }, { [option]: types[type] }), @@ -59,7 +55,7 @@ server.on('stream', common.mustCall((stream) => { // Should throw if :status 204, 205 or 304 [204, 205, 304].forEach((status) => common.expectsError( () => stream.respondWithFile(fname, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', + 'content-type': 'text/plain', ':status': status, }), { @@ -68,31 +64,11 @@ server.on('stream', common.mustCall((stream) => { } )); - // should emit an error on the stream if headers aren't valid - stream.respondWithFile(fname, { - [HTTP2_HEADER_METHOD]: 'POST' - }, { - statCheck: common.mustCall(() => { - // give time to the current test case to finish - process.nextTick(continueTest, stream); - return true; - }) - }); - stream.once('error', common.expectsError({ - code: 'ERR_HTTP2_INVALID_PSEUDOHEADER', - type: Error, - message: '":method" is an invalid pseudoheader or is used incorrectly' - })); -})); - -function continueTest(stream) { // Should throw if headers already sent - stream.respond({ - ':status': 200, - }); + stream.respond({ ':status': 200 }); common.expectsError( () => stream.respondWithFile(fname, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }), { code: 'ERR_HTTP2_HEADERS_SENT', @@ -104,21 +80,21 @@ function continueTest(stream) { stream.destroy(); common.expectsError( () => stream.respondWithFile(fname, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }), { code: 'ERR_HTTP2_INVALID_STREAM', message: 'The stream has been destroyed' } ); -} +})); server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); req.on('close', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-fd-errors.js b/test/parallel/test-http2-respond-file-fd-errors.js index 9458b2f49af087..44876b60e1c4cb 100644 --- a/test/parallel/test-http2-respond-file-fd-errors.js +++ b/test/parallel/test-http2-respond-file-fd-errors.js @@ -7,11 +7,6 @@ const fixtures = require('../common/fixtures'); const http2 = require('http2'); const fs = require('fs'); -const { - HTTP2_HEADER_CONTENT_TYPE, - HTTP2_HEADER_METHOD -} = http2.constants; - const optionsWithTypeError = { offset: 'number', length: 'number', @@ -43,7 +38,7 @@ server.on('stream', common.mustCall((stream) => { common.expectsError( () => stream.respondWithFD(types[type], { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }), { type: TypeError, @@ -62,7 +57,7 @@ server.on('stream', common.mustCall((stream) => { common.expectsError( () => stream.respondWithFD(fd, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }, { [option]: types[type] }), @@ -79,7 +74,7 @@ server.on('stream', common.mustCall((stream) => { // Should throw if :status 204, 205 or 304 [204, 205, 304].forEach((status) => common.expectsError( () => stream.respondWithFD(fd, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', + 'content-type': 'text/plain', ':status': status, }), { @@ -89,35 +84,11 @@ server.on('stream', common.mustCall((stream) => { } )); - // should emit an error on the stream if headers aren't valid - stream.respondWithFD(fd, { - [HTTP2_HEADER_METHOD]: 'POST' - }, { - statCheck() { - return true; - } - }); - stream.once('error', common.expectsError({ - code: 'ERR_HTTP2_INVALID_PSEUDOHEADER', - type: Error, - message: '":method" is an invalid pseudoheader or is used incorrectly' - })); - stream.respondWithFD(fd, { - [HTTP2_HEADER_METHOD]: 'POST' - }); - stream.once('error', common.expectsError({ - code: 'ERR_HTTP2_INVALID_PSEUDOHEADER', - type: Error, - message: '":method" is an invalid pseudoheader or is used incorrectly' - })); - // Should throw if headers already sent - stream.respond({ - ':status': 200, - }); + stream.respond(); common.expectsError( () => stream.respondWithFD(fd, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }), { code: 'ERR_HTTP2_HEADERS_SENT', @@ -130,7 +101,7 @@ server.on('stream', common.mustCall((stream) => { stream.destroy(); common.expectsError( () => stream.respondWithFD(fd, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + 'content-type': 'text/plain' }), { code: 'ERR_HTTP2_INVALID_STREAM', @@ -145,7 +116,7 @@ server.listen(0, common.mustCall(() => { const req = client.request(); req.on('close', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-fd-invalid.js b/test/parallel/test-http2-respond-file-fd-invalid.js index f3bcab8904bee4..77a4d3df00d0d6 100644 --- a/test/parallel/test-http2-respond-file-fd-invalid.js +++ b/test/parallel/test-http2-respond-file-fd-invalid.js @@ -31,7 +31,7 @@ server.listen(0, () => { req.on('data', common.mustNotCall()); req.on('end', common.mustCall(() => { assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-fd-range.js b/test/parallel/test-http2-respond-file-fd-range.js index 8479dca518558e..2dd73e0001544c 100644 --- a/test/parallel/test-http2-respond-file-fd-range.js +++ b/test/parallel/test-http2-respond-file-fd-range.js @@ -9,6 +9,7 @@ const fixtures = require('../common/fixtures'); const http2 = require('http2'); const assert = require('assert'); const fs = require('fs'); +const Countdown = require('../common/countdown'); const { HTTP2_HEADER_CONTENT_TYPE, @@ -39,7 +40,7 @@ server.on('stream', (stream, headers) => { statCheck: common.mustCall((stat, headers, options) => { assert.strictEqual(options.length, length); assert.strictEqual(options.offset, offset); - headers[HTTP2_HEADER_CONTENT_LENGTH] = + headers['content-length'] = Math.min(options.length, stat.size - offset); }), offset: offset, @@ -47,23 +48,21 @@ server.on('stream', (stream, headers) => { }); }); server.on('close', common.mustCall(() => fs.closeSync(fd))); + server.listen(0, () => { const client = http2.connect(`http://localhost:${server.address().port}`); - let remaining = 2; - function maybeClose() { - if (--remaining === 0) { - client.destroy(); - server.close(); - } - } + const countdown = new Countdown(2, () => { + client.close(); + server.close(); + }); { const req = client.request({ range: 'bytes=8-11' }); req.on('response', common.mustCall((headers) => { - assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); - assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], 3); + assert.strictEqual(headers['content-type'], 'text/plain'); + assert.strictEqual(+headers['content-length'], 3); })); req.setEncoding('utf8'); let check = ''; @@ -71,7 +70,7 @@ server.listen(0, () => { req.on('end', common.mustCall(() => { assert.strictEqual(check, data.toString('utf8', 8, 11)); })); - req.on('close', common.mustCall(maybeClose)); + req.on('close', common.mustCall(() => countdown.dec())); req.end(); } @@ -88,7 +87,7 @@ server.listen(0, () => { req.on('end', common.mustCall(() => { assert.strictEqual(check, data.toString('utf8', 8, 28)); })); - req.on('close', common.mustCall(maybeClose)); + req.on('close', common.mustCall(() => countdown.dec())); req.end(); } diff --git a/test/parallel/test-http2-respond-file-fd.js b/test/parallel/test-http2-respond-file-fd.js index 303d25be3f2b66..7d4395bbc360aa 100644 --- a/test/parallel/test-http2-respond-file-fd.js +++ b/test/parallel/test-http2-respond-file-fd.js @@ -40,7 +40,7 @@ server.listen(0, () => { req.on('data', (chunk) => check += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(check, data.toString('utf8')); - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file-push.js b/test/parallel/test-http2-respond-file-push.js index 4f7b179faf81a8..a5229beb07d1a7 100644 --- a/test/parallel/test-http2-respond-file-push.js +++ b/test/parallel/test-http2-respond-file-push.js @@ -29,7 +29,8 @@ server.on('stream', (stream) => { stream.pushStream({ ':path': '/file.txt', ':method': 'GET' - }, (stream) => { + }, (err, stream) => { + assert.ifError(err); stream.respondWithFD(fd, { [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', [HTTP2_HEADER_CONTENT_LENGTH]: stat.size, @@ -50,7 +51,7 @@ server.listen(0, () => { function maybeClose() { if (--expected === 0) { server.close(); - client.destroy(); + client.close(); } } diff --git a/test/parallel/test-http2-respond-file-range.js b/test/parallel/test-http2-respond-file-range.js index a5995cbba77c1c..4e6a6074514f14 100644 --- a/test/parallel/test-http2-respond-file-range.js +++ b/test/parallel/test-http2-respond-file-range.js @@ -46,7 +46,7 @@ server.listen(0, () => { req.on('data', (chunk) => check += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(check, data.toString('utf8', 8, 11)); - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-file.js b/test/parallel/test-http2-respond-file.js index c2f513b7cae2b7..9ad8e7a69648dc 100644 --- a/test/parallel/test-http2-respond-file.js +++ b/test/parallel/test-http2-respond-file.js @@ -45,7 +45,7 @@ server.listen(0, () => { req.on('data', (chunk) => check += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(check, data.toString('utf8')); - client.destroy(); + client.close(); server.close(); })); req.end(); diff --git a/test/parallel/test-http2-respond-no-data.js b/test/parallel/test-http2-respond-no-data.js index d891fe4e8ddd2b..9572bdffe54927 100644 --- a/test/parallel/test-http2-respond-no-data.js +++ b/test/parallel/test-http2-respond-no-data.js @@ -27,7 +27,7 @@ function makeRequest() { req.resume(); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); if (!status.length) { server.close(); diff --git a/test/parallel/test-http2-respond-with-fd-errors.js b/test/parallel/test-http2-respond-with-fd-errors.js index c8ecfcf5f3490e..0b215134663bda 100644 --- a/test/parallel/test-http2-respond-with-fd-errors.js +++ b/test/parallel/test-http2-respond-with-fd-errors.js @@ -82,12 +82,18 @@ function runTest(test) { const client = http2.connect(url); const req = client.request(headers); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + })); + currentError = test; req.resume(); req.end(); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); if (!tests.length) { server.close(); diff --git a/test/parallel/test-http2-response-splitting.js b/test/parallel/test-http2-response-splitting.js index 1d9b616105f450..9613eca9636ae4 100644 --- a/test/parallel/test-http2-response-splitting.js +++ b/test/parallel/test-http2-response-splitting.js @@ -55,7 +55,7 @@ server.listen(0, common.mustCall(() => { function maybeClose() { if (remaining === 0) { server.close(); - client.destroy(); + client.close(); } } diff --git a/test/parallel/test-http2-rststream-errors.js b/test/parallel/test-http2-rststream-errors.js deleted file mode 100644 index f53956ce998e93..00000000000000 --- a/test/parallel/test-http2-rststream-errors.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const http2 = require('http2'); -const { - constants, - Http2Stream, - nghttp2ErrorString -} = process.binding('http2'); - -// tests error handling within rstStream -// - every other NGHTTP2 error from binding (should emit stream error) - -const specificTestKeys = []; -const specificTests = []; - -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 submitRstStream because we only care about testing error handling -Http2Stream.prototype.rstStream = () => 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.session.destroy(); - })); - } else { - stream.session.once('error', errorMustCall); - stream.on('error', errorMustNotCall); - } - - stream.rstStream(); -}, 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(); - - if (currentError.type === 'stream') { - req.on('error', common.mustCall()); - } - - req.on('end', common.mustCall(() => { - client.destroy(); - - if (!tests.length) { - server.close(); - } else { - runTest(tests.shift()); - } - })); -} diff --git a/test/parallel/test-http2-serve-file.js b/test/parallel/test-http2-serve-file.js index af82360e464b31..7b73fe639e0cc5 100644 --- a/test/parallel/test-http2-serve-file.js +++ b/test/parallel/test-http2-serve-file.js @@ -48,7 +48,7 @@ server.listen(0, () => { let remaining = 2; function maybeClose() { if (--remaining === 0) { - client.destroy(); + client.close(); server.close(); } } diff --git a/test/parallel/test-http2-server-errors.js b/test/parallel/test-http2-server-errors.js index 7d7db6a24538fd..a3586bd64d46e7 100644 --- a/test/parallel/test-http2-server-errors.js +++ b/test/parallel/test-http2-server-errors.js @@ -6,7 +6,6 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const h2 = require('http2'); -const { Http2Stream } = require('internal/http2/core'); // Errors should not be reported both in Http2ServerRequest // and Http2ServerResponse @@ -29,11 +28,6 @@ const { Http2Stream } = require('internal/http2/core'); server.close(); })); - server.on('streamError', common.mustCall(function(err, stream) { - assert.strictEqual(err, expected); - assert.strictEqual(stream instanceof Http2Stream, true); - })); - server.listen(0, common.mustCall(function() { const port = server.address().port; @@ -70,11 +64,6 @@ const { Http2Stream } = require('internal/http2/core'); server.close(); })); - server.on('streamError', common.mustCall(function(err, stream) { - assert.strictEqual(err, expected); - assert.strictEqual(stream instanceof Http2Stream, true); - })); - server.listen(0, common.mustCall(function() { const port = server.address().port; diff --git a/test/parallel/test-http2-server-http1-client.js b/test/parallel/test-http2-server-http1-client.js index ef3a79c0fd143a..34a8f48b5e130d 100644 --- a/test/parallel/test-http2-server-http1-client.js +++ b/test/parallel/test-http2-server-http1-client.js @@ -12,11 +12,14 @@ const server = http2.createServer(); server.on('stream', common.mustNotCall()); server.on('session', common.mustCall((session) => { session.on('close', common.mustCall()); + session.on('error', common.expectsError({ + code: 'ERR_HTTP2_ERROR', + type: Error, + message: 'Received bad client magic byte string' + })); })); server.listen(0, common.mustCall(() => { const req = http.get(`http://localhost:${server.address().port}`); - req.on('error', (error) => { - server.close(); - }); + req.on('error', (error) => server.close()); })); diff --git a/test/parallel/test-http2-server-push-disabled.js b/test/parallel/test-http2-server-push-disabled.js index 33390f2ecae886..9a0b748d354276 100644 --- a/test/parallel/test-http2-server-push-disabled.js +++ b/test/parallel/test-http2-server-push-disabled.js @@ -48,7 +48,7 @@ server.listen(0, common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end(); })); diff --git a/test/parallel/test-http2-server-push-stream-errors-args.js b/test/parallel/test-http2-server-push-stream-errors-args.js index 73fa064b439199..f752f44310c0df 100644 --- a/test/parallel/test-http2-server-push-stream-errors-args.js +++ b/test/parallel/test-http2-server-push-stream-errors-args.js @@ -50,7 +50,7 @@ server.listen(0, common.mustCall(() => { req.on('end', common.mustCall(() => { assert.strictEqual(data, 'test'); server.close(); - client.destroy(); + client.close(); })); req.end(); })); diff --git a/test/parallel/test-http2-server-push-stream-errors.js b/test/parallel/test-http2-server-push-stream-errors.js index 56e329dcff1cd2..7eaf4dc94d15e2 100644 --- a/test/parallel/test-http2-server-push-stream-errors.js +++ b/test/parallel/test-http2-server-push-stream-errors.js @@ -34,9 +34,8 @@ const specificTests = [ { ngError: constants.NGHTTP2_ERR_STREAM_CLOSED, error: { - code: 'ERR_HTTP2_STREAM_CLOSED', - type: Error, - message: 'The stream is already closed' + code: 'ERR_HTTP2_INVALID_STREAM', + type: Error }, type: 'stream' }, @@ -66,47 +65,25 @@ Http2Stream.prototype.pushPromise = () => 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.pushStream({}, () => {}); + stream.pushStream({}, common.expectsError(currentError.error)); + stream.respond(); + stream.end(); }, 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 url = `http://localhost:${server.address().port}`; const client = http2.connect(url); - const req = client.request(headers); + const req = client.request(); currentError = test; req.resume(); req.end(); - req.on('end', common.mustCall(() => { - client.destroy(); + req.on('close', common.mustCall(() => { + client.close(); if (!tests.length) { server.close(); diff --git a/test/parallel/test-http2-server-push-stream-head.js b/test/parallel/test-http2-server-push-stream-head.js index c2fc8db4a92eba..cd2276746f4bdd 100644 --- a/test/parallel/test-http2-server-push-stream-head.js +++ b/test/parallel/test-http2-server-push-stream-head.js @@ -5,6 +5,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const http2 = require('http2'); +const Countdown = require('../common/countdown'); // Check that pushStream handles method HEAD correctly // - stream should end immediately (no body) @@ -17,8 +18,10 @@ server.on('stream', common.mustCall((stream, headers) => { ':scheme': 'http', ':method': 'HEAD', ':authority': `localhost:${port}`, - }, common.mustCall((push, headers) => { + }, common.mustCall((err, push, headers) => { assert.strictEqual(push._writableState.ended, true); + push.respond(); + assert(!push.write('test')); stream.end('test'); })); } @@ -30,15 +33,26 @@ server.on('stream', common.mustCall((stream, headers) => { server.listen(0, common.mustCall(() => { const port = server.address().port; - const headers = { ':path': '/' }; const client = http2.connect(`http://localhost:${port}`); - const req = client.request(headers); + + const countdown = new Countdown(2, () => { + server.close(); + client.close(); + }); + + const req = client.request(); req.setEncoding('utf8'); client.on('stream', common.mustCall((stream, headers) => { + assert.strictEqual(headers[':method'], 'HEAD'); assert.strictEqual(headers[':scheme'], 'http'); assert.strictEqual(headers[':path'], '/'); assert.strictEqual(headers[':authority'], `localhost:${port}`); + stream.on('push', common.mustCall(() => { + stream.on('data', common.mustNotCall()); + stream.on('end', common.mustCall()); + })); + stream.on('close', common.mustCall(() => countdown.dec())); })); let data = ''; @@ -46,8 +60,7 @@ server.listen(0, common.mustCall(() => { req.on('data', common.mustCall((d) => data += d)); req.on('end', common.mustCall(() => { assert.strictEqual(data, 'test'); - server.close(); - client.destroy(); })); + req.on('close', common.mustCall(() => countdown.dec())); req.end(); })); diff --git a/test/parallel/test-http2-server-push-stream.js b/test/parallel/test-http2-server-push-stream.js index 395743869198ca..6ac10cae77f951 100644 --- a/test/parallel/test-http2-server-push-stream.js +++ b/test/parallel/test-http2-server-push-stream.js @@ -14,7 +14,8 @@ server.on('stream', common.mustCall((stream, headers) => { ':scheme': 'http', ':path': '/foobar', ':authority': `localhost:${port}`, - }, common.mustCall((push, headers) => { + }, common.mustCall((err, push, headers) => { + assert.ifError(err); push.respond({ 'content-type': 'text/html', ':status': 200, @@ -53,7 +54,7 @@ server.listen(0, common.mustCall(() => { req.on('end', common.mustCall(() => { assert.strictEqual(data, 'test'); server.close(); - client.destroy(); + client.close(); })); req.end(); })); diff --git a/test/parallel/test-http2-server-rst-before-respond.js b/test/parallel/test-http2-server-rst-before-respond.js index 950beea4eb39ab..74181f4ffa0bbe 100644 --- a/test/parallel/test-http2-server-rst-before-respond.js +++ b/test/parallel/test-http2-server-rst-before-respond.js @@ -12,35 +12,26 @@ const server = h2.createServer(); server.on('stream', common.mustCall(onStream)); function onStream(stream, headers, flags) { - stream.rstStream(); + stream.close(); common.expectsError(() => { stream.additionalHeaders({ ':status': 123, abc: 123 }); - }, { - code: 'ERR_HTTP2_INVALID_STREAM', - message: /^The stream has been destroyed$/ - }); + }, { code: 'ERR_HTTP2_INVALID_STREAM' }); } server.listen(0); server.on('listening', common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`); - - const req = client.request({ ':path': '/' }); - + const req = client.request(); req.on('headers', common.mustNotCall()); - req.on('close', common.mustCall((code) => { assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, code); server.close(); - client.destroy(); + client.close(); })); - req.on('response', common.mustNotCall()); - })); diff --git a/test/parallel/test-http2-server-rst-stream.js b/test/parallel/test-http2-server-rst-stream.js index 4b04f29c8ec7c0..c2d938c22f4483 100644 --- a/test/parallel/test-http2-server-rst-stream.js +++ b/test/parallel/test-http2-server-rst-stream.js @@ -16,39 +16,38 @@ const { } = http2.constants; const tests = [ - ['rstStream', NGHTTP2_NO_ERROR, false], - ['rstWithNoError', NGHTTP2_NO_ERROR, false], - ['rstWithProtocolError', NGHTTP2_PROTOCOL_ERROR, true], - ['rstWithCancel', NGHTTP2_CANCEL, false], - ['rstWithRefuse', NGHTTP2_REFUSED_STREAM, true], - ['rstWithInternalError', NGHTTP2_INTERNAL_ERROR, true] + [NGHTTP2_NO_ERROR, false], + [NGHTTP2_NO_ERROR, false], + [NGHTTP2_PROTOCOL_ERROR, true], + [NGHTTP2_CANCEL, false], + [NGHTTP2_REFUSED_STREAM, true], + [NGHTTP2_INTERNAL_ERROR, true] ]; const server = http2.createServer(); server.on('stream', (stream, headers) => { - const method = headers['rstmethod']; - stream[method](); + stream.close(headers['rstcode'] | 0); }); server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const countdown = new Countdown(tests.length, common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); tests.forEach((test) => { const req = client.request({ ':method': 'POST', - rstmethod: test[0] + rstcode: test[0] }); req.on('close', common.mustCall((code) => { - assert.strictEqual(code, test[1]); + assert.strictEqual(code, test[0]); countdown.dec(); })); req.on('aborted', common.mustCall()); - if (test[2]) + if (test[1]) req.on('error', common.mustCall()); else req.on('error', common.mustNotCall()); diff --git a/test/parallel/test-http2-server-sessionerror.js b/test/parallel/test-http2-server-sessionerror.js new file mode 100644 index 00000000000000..525eb2e6efd11a --- /dev/null +++ b/test/parallel/test-http2-server-sessionerror.js @@ -0,0 +1,48 @@ +// Flags: --expose-internals + +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const { kSocket } = require('internal/http2/util'); + +const server = http2.createServer(); +server.on('stream', common.mustNotCall()); + +let test = 0; + +server.on('session', common.mustCall((session) => { + switch (++test) { + case 1: + server.on('error', common.mustNotCall()); + session.on('error', common.expectsError({ + type: Error, + message: 'test' + })); + session[kSocket].emit('error', new Error('test')); + break; + case 2: + // If the server does not have a socketError listener, + // error will be silent on the server but will close + // the session + session[kSocket].emit('error', new Error('test')); + break; + } +}, 2)); + +server.listen(0, common.mustCall(() => { + const url = `http://localhost:${server.address().port}`; + http2.connect(url) + // An ECONNRESET error may occur depending on the platform (due largely + // to differences in the timing of socket closing). Do not wrap this in + // a common must call. + .on('error', () => {}) + .on('close', () => { + server.removeAllListeners('error'); + http2.connect(url) + .on('error', () => {}) + .on('close', () => server.close()); + }); +})); diff --git a/test/parallel/test-http2-server-set-header.js b/test/parallel/test-http2-server-set-header.js index ed27638f6849f4..4b6228053f8ece 100644 --- a/test/parallel/test-http2-server-set-header.js +++ b/test/parallel/test-http2-server-set-header.js @@ -29,7 +29,7 @@ server.listen(0, common.mustCall(() => { req.on('end', () => { assert.strictEqual(body, data); server.close(); - client.destroy(); + client.close(); }); req.end(); })); diff --git a/test/parallel/test-http2-server-shutdown-before-respond.js b/test/parallel/test-http2-server-shutdown-before-respond.js index c3ad9714b5f39b..33f224fc69a9d5 100644 --- a/test/parallel/test-http2-server-shutdown-before-respond.js +++ b/test/parallel/test-http2-server-shutdown-before-respond.js @@ -11,24 +11,26 @@ const server = h2.createServer(); server.on('stream', common.mustCall(onStream)); function onStream(stream, headers, flags) { - const session = stream.session; - stream.session.shutdown({ graceful: true }, common.mustCall(() => { - session.destroy(); - })); - stream.respond({}); + stream.session.goaway(1); + stream.respond(); stream.end('data'); } server.listen(0); server.on('listening', common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`); client.on('goaway', common.mustCall()); + client.on('error', common.expectsError({ + code: 'ERR_HTTP2_SESSION_ERROR' + })); const req = client.request(); - + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_SESSION_ERROR' + })); req.resume(); + req.on('data', common.mustNotCall()); req.on('end', common.mustCall(() => server.close())); })); diff --git a/test/parallel/test-http2-server-shutdown-options-errors.js b/test/parallel/test-http2-server-shutdown-options-errors.js index 2bf2b47dd73dec..6cee8f25729535 100644 --- a/test/parallel/test-http2-server-shutdown-options-errors.js +++ b/test/parallel/test-http2-server-shutdown-options-errors.js @@ -7,55 +7,63 @@ const http2 = require('http2'); const server = http2.createServer(); -const optionsToTest = { - opaqueData: 'Uint8Array', - graceful: 'boolean', - errorCode: 'number', - lastStreamID: 'number' -}; +const types = [ + true, + {}, + [], + null, + new Date() +]; -const types = { - boolean: true, - number: 1, - object: {}, - array: [], - null: null, - Uint8Array: Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]) -}; +server.on('stream', common.mustCall((stream) => { + const session = stream.session; -server.on( - 'stream', - common.mustCall((stream) => { - Object.keys(optionsToTest).forEach((option) => { - Object.keys(types).forEach((type) => { - if (type === optionsToTest[option]) { - return; - } - common.expectsError( - () => - stream.session.shutdown( - { [option]: types[type] }, - common.mustNotCall() - ), - { - type: TypeError, - code: 'ERR_INVALID_OPT_VALUE', - message: `The value "${String(types[type])}" is invalid ` + - `for option "${option}"` - } - ); - }); - }); - stream.session.destroy(); - }) -); + types.forEach((i) => { + common.expectsError( + () => session.goaway(i), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "code" argument must be of type number' + } + ); + common.expectsError( + () => session.goaway(0, i), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "lastStreamID" argument must be of type number' + } + ); + common.expectsError( + () => session.goaway(0, 0, i), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "opaqueData" argument must be one of type Buffer, ' + + 'TypedArray, or DataView' + } + ); + }); + + stream.session.destroy(); +})); server.listen( 0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); + // On certain operating systems, an ECONNRESET may occur. We do not need + // to test for it here. Do not make this a mustCall + client.on('error', () => {}); const req = client.request(); + // On certain operating systems, an ECONNRESET may occur. We do not need + // to test for it here. Do not make this a mustCall + req.on('error', () => {}); req.resume(); - req.on('end', common.mustCall(() => server.close())); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); }) ); diff --git a/test/parallel/test-http2-server-shutdown-redundant.js b/test/parallel/test-http2-server-shutdown-redundant.js index 708be0868c2f9e..f47a0d2c951c86 100644 --- a/test/parallel/test-http2-server-shutdown-redundant.js +++ b/test/parallel/test-http2-server-shutdown-redundant.js @@ -3,27 +3,38 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const assert = require('assert'); const http2 = require('http2'); const server = http2.createServer(); -// Test blank return when a stream.session.shutdown is called twice -// Also tests stream.session.shutdown with just a callback function (no options) server.on('stream', common.mustCall((stream) => { - stream.session.shutdown(common.mustCall(() => { - assert.strictEqual( - stream.session.shutdown(common.mustNotCall()), - undefined + const session = stream.session; + session.goaway(1); + session.goaway(2); + stream.session.on('close', common.mustCall(() => { + common.expectsError( + () => session.goaway(3), + { + code: 'ERR_HTTP2_INVALID_SESSION', + type: Error + } ); })); - stream.session.shutdown(common.mustNotCall()); })); server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); + client.on('error', common.expectsError({ + code: 'ERR_HTTP2_SESSION_ERROR' + })); const req = client.request(); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_SESSION_ERROR' + })); req.resume(); - req.on('end', common.mustCall(() => server.close())); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); })); diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js index 8291c415284571..03afc1957b8af4 100644 --- a/test/parallel/test-http2-server-socket-destroy.js +++ b/test/parallel/test-http2-server-socket-destroy.js @@ -9,22 +9,13 @@ const assert = require('assert'); const h2 = require('http2'); const { kSocket } = require('internal/http2/util'); -const { - HTTP2_HEADER_METHOD, - HTTP2_HEADER_PATH, - HTTP2_METHOD_POST -} = h2.constants; - const server = h2.createServer(); // we use the lower-level API here server.on('stream', common.mustCall(onStream)); function onStream(stream) { - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); + stream.respond(); stream.write('test'); const socket = stream.session[kSocket]; @@ -32,6 +23,7 @@ function onStream(stream) { // When the socket is destroyed, the close events must be triggered // on the socket, server and session. socket.on('close', common.mustCall()); + stream.on('close', common.mustCall()); server.on('close', common.mustCall()); stream.session.on('close', common.mustCall(() => server.close())); @@ -40,23 +32,25 @@ function onStream(stream) { assert.notStrictEqual(stream.session, undefined); socket.destroy(); - stream.on('destroy', common.mustCall(() => { - assert.strictEqual(stream.session, undefined); - })); } server.listen(0); server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); + // The client may have an ECONNRESET error here depending on the operating + // system, due mainly to differences in the timing of socket closing. Do + // not wrap this in a common mustCall. + client.on('error', () => {}); + client.on('close', common.mustCall()); - const req = client.request({ - [HTTP2_HEADER_PATH]: '/', - [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); + const req = client.request({ ':method': 'POST' }); + // The client may have an ECONNRESET error here depending on the operating + // system, due mainly to differences in the timing of socket closing. Do + // not wrap this in a common mustCall. + req.on('error', () => {}); req.on('aborted', common.mustCall()); req.resume(); req.on('end', common.mustCall()); - - client.on('close', common.mustCall()); })); diff --git a/test/parallel/test-http2-server-socketerror.js b/test/parallel/test-http2-server-socketerror.js deleted file mode 100644 index 9f52b9280d2779..00000000000000 --- a/test/parallel/test-http2-server-socketerror.js +++ /dev/null @@ -1,56 +0,0 @@ -// Flags: --expose-internals - -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const assert = require('assert'); -const http2 = require('http2'); -const { kSocket } = require('internal/http2/util'); - -const server = http2.createServer(); -server.on('stream', common.mustCall((stream) => { - stream.respond(); - stream.end('ok'); -})); -server.on('session', common.mustCall((session) => { - // First, test that the socketError event is forwarded to the session object - // and not the server object. - const handler = common.mustCall((error, socket) => { - common.expectsError({ - type: Error, - message: 'test' - })(error); - assert.strictEqual(socket, session[kSocket]); - }); - const isNotCalled = common.mustNotCall(); - session.on('socketError', handler); - server.on('socketError', isNotCalled); - session[kSocket].emit('error', new Error('test')); - session.removeListener('socketError', handler); - server.removeListener('socketError', isNotCalled); - - // Second, test that the socketError is forwarded to the server object when - // no socketError listener is registered for the session - server.on('socketError', common.mustCall((error, socket, session) => { - common.expectsError({ - type: Error, - message: 'test' - })(error); - assert.strictEqual(socket, session[kSocket]); - assert.strictEqual(session, session); - })); - session[kSocket].emit('error', new Error('test')); -})); - -server.listen(0, common.mustCall(() => { - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - req.resume(); - req.on('end', common.mustCall()); - req.on('close', common.mustCall(() => { - client.destroy(); - server.close(); - })); -})); diff --git a/test/parallel/test-http2-server-stream-session-destroy.js b/test/parallel/test-http2-server-stream-session-destroy.js index 24d064a448f87d..5eb04a8d376635 100644 --- a/test/parallel/test-http2-server-stream-session-destroy.js +++ b/test/parallel/test-http2-server-stream-session-destroy.js @@ -8,56 +8,41 @@ const h2 = require('http2'); const server = h2.createServer(); -server.on( - 'stream', - common.mustCall((stream) => { - stream.session.destroy(); - - // Test that stream.state getter returns an empty object - // when the stream session has been destroyed - assert.deepStrictEqual({}, stream.state); - - // Test that ERR_HTTP2_INVALID_STREAM is thrown while calling - // stream operations after the stream session has been destroyed - const invalidStreamError = { - type: Error, - code: 'ERR_HTTP2_INVALID_STREAM', - message: 'The stream has been destroyed' - }; - common.expectsError(() => stream.additionalHeaders(), invalidStreamError); - common.expectsError(() => stream.priority(), invalidStreamError); - common.expectsError( - () => stream.pushStream({}, common.mustNotCall()), - invalidStreamError - ); - common.expectsError(() => stream.respond(), invalidStreamError); - common.expectsError(() => stream.write('data'), invalidStreamError); - - // Test that ERR_HTTP2_INVALID_SESSION is thrown while calling - // session operations after the stream session has been destroyed - const invalidSessionError = { - type: Error, - code: 'ERR_HTTP2_INVALID_SESSION', - message: 'The session has been destroyed' - }; - 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.settings(), invalidSessionError); - common.expectsError(() => session.shutdown(), invalidSessionError); - }, stream.session); - }) -); - -server.listen( - 0, - common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - req.resume(); - req.on('end', common.mustCall(() => server.close())); - }) -); +server.on('stream', common.mustCall((stream) => { + assert(stream.session); + stream.session.destroy(); + assert.strictEqual(stream.session, undefined); + + // Test that stream.state getter returns an empty object + // when the stream session has been destroyed + assert.deepStrictEqual({}, stream.state); + + // Test that ERR_HTTP2_INVALID_STREAM is thrown while calling + // stream operations after the stream session has been destroyed + const invalidStreamError = { + type: Error, + code: 'ERR_HTTP2_INVALID_STREAM', + message: 'The stream has been destroyed' + }; + common.expectsError(() => stream.additionalHeaders(), invalidStreamError); + common.expectsError(() => stream.priority(), invalidStreamError); + common.expectsError(() => stream.respond(), invalidStreamError); + common.expectsError( + () => stream.pushStream({}, common.mustNotCall()), + { + code: 'ERR_HTTP2_PUSH_DISABLED', + type: Error + } + ); + assert.strictEqual(stream.write('data'), false); +})); + +server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.on('error', () => {}); + const req = client.request(); + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => server.close())); + req.on('error', () => {}); +})); diff --git a/test/parallel/test-http2-server-timeout.js b/test/parallel/test-http2-server-timeout.js index 28ab6efb87f6c1..581a409ce9171d 100755 --- a/test/parallel/test-http2-server-timeout.js +++ b/test/parallel/test-http2-server-timeout.js @@ -9,7 +9,7 @@ const server = http2.createServer(); server.setTimeout(common.platformTimeout(1)); const onServerTimeout = common.mustCall((session) => { - session.destroy(); + session.close(() => session.destroy()); }); server.on('stream', common.mustNotCall()); @@ -18,10 +18,14 @@ server.once('timeout', onServerTimeout); server.listen(0, common.mustCall(() => { const url = `http://localhost:${server.address().port}`; const client = http2.connect(url); + // Because of the timeout, an ECONRESET error may or may not happen here. + // Keep this as a non-op and do not use common.mustCall() + client.on('error', () => {}); client.on('close', common.mustCall(() => { - const client2 = http2.connect(url); + // Because of the timeout, an ECONRESET error may or may not happen here. + // Keep this as a non-op and do not use common.mustCall() + client2.on('error', () => {}); client2.on('close', common.mustCall(() => server.close())); - })); })); diff --git a/test/parallel/test-http2-session-gc-while-write-scheduled.js b/test/parallel/test-http2-session-gc-while-write-scheduled.js new file mode 100644 index 00000000000000..bb23760cebf967 --- /dev/null +++ b/test/parallel/test-http2-session-gc-while-write-scheduled.js @@ -0,0 +1,32 @@ +// Flags: --expose-gc + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const makeDuplexPair = require('../common/duplexpair'); + +// This tests that running garbage collection while an Http2Session has +// a write *scheduled*, it will survive that garbage collection. + +{ + // This creates a session and schedules a write (for the settings frame). + let client = http2.connect('http://localhost:80', { + createConnection: common.mustCall(() => makeDuplexPair().clientSide) + }); + + // First, wait for any nextTicks() and their responses + // from the `connect()` call to run. + tick(10, () => { + // This schedules a write. + client.settings(http2.getDefaultSettings()); + client = null; + global.gc(); + }); +} + +function tick(n, cb) { + if (n--) setImmediate(tick, n, cb); + else cb(); +} diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js index 53ff44dd9ec2fe..75fcc1942104ac 100644 --- a/test/parallel/test-http2-session-settings.js +++ b/test/parallel/test-http2-session-settings.js @@ -68,71 +68,60 @@ server.listen( const req = client.request(headers); - req.on( - 'connect', - common.mustCall(() => { - // pendingSettingsAck will be true if a SETTINGS frame - // has been sent but we are still waiting for an acknowledgement - assert(client.pendingSettingsAck); - }) - ); + req.on('ready', common.mustCall(() => { + // pendingSettingsAck will be true if a SETTINGS frame + // has been sent but we are still waiting for an acknowledgement + assert(client.pendingSettingsAck); + })); // State will only be valid after connect event is emitted - req.on( - 'ready', - common.mustCall(() => { - assert.doesNotThrow(() => { - client.settings({ - maxHeaderListSize: 1 - }); - }); + req.on('ready', common.mustCall(() => { + assert.doesNotThrow(() => { + client.settings({ maxHeaderListSize: 1 }, common.mustCall()); + }); - // Verify valid error ranges - [ - ['headerTableSize', -1], - ['headerTableSize', 2 ** 32], - ['initialWindowSize', -1], - ['initialWindowSize', 2 ** 32], - ['maxFrameSize', 16383], - ['maxFrameSize', 2 ** 24], - ['maxHeaderListSize', -1], - ['maxHeaderListSize', 2 ** 32] - ].forEach((i) => { - const settings = {}; - settings[i[0]] = i[1]; - common.expectsError( - () => client.settings(settings), - { - type: RangeError, - code: 'ERR_HTTP2_INVALID_SETTING_VALUE', - message: `Invalid value for setting "${i[0]}": ${i[1]}` - } - ); - }); + // Verify valid error ranges + [ + ['headerTableSize', -1], + ['headerTableSize', 2 ** 32], + ['initialWindowSize', -1], + ['initialWindowSize', 2 ** 32], + ['maxFrameSize', 16383], + ['maxFrameSize', 2 ** 24], + ['maxHeaderListSize', -1], + ['maxHeaderListSize', 2 ** 32] + ].forEach((i) => { + const settings = {}; + settings[i[0]] = i[1]; + common.expectsError( + () => client.settings(settings), + { + type: RangeError, + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + message: `Invalid value for setting "${i[0]}": ${i[1]}` + } + ); + }); - // error checks for enablePush - [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => { - common.expectsError( - () => client.settings({ enablePush: i }), - { - type: TypeError, - code: 'ERR_HTTP2_INVALID_SETTING_VALUE', - message: `Invalid value for setting "enablePush": ${i}` - } - ); - }); - }) - ); + // error checks for enablePush + [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => { + common.expectsError( + () => client.settings({ enablePush: i }), + { + type: TypeError, + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + message: `Invalid value for setting "enablePush": ${i}` + } + ); + }); + })); req.on('response', common.mustCall()); req.resume(); - req.on( - 'end', - common.mustCall(() => { - server.close(); - client.destroy(); - }) - ); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); req.end(); }) ); diff --git a/test/parallel/test-http2-session-stream-state.js b/test/parallel/test-http2-session-stream-state.js index 9bbac3f482cbcf..612feb8cf1e2ca 100644 --- a/test/parallel/test-http2-session-stream-state.js +++ b/test/parallel/test-http2-session-stream-state.js @@ -57,7 +57,7 @@ server.on('listening', common.mustCall(() => { const req = client.request(headers); // State will only be valid after connect event is emitted - req.on('connect', common.mustCall(() => { + req.on('ready', common.mustCall(() => { // Test Stream State. { @@ -91,7 +91,7 @@ server.on('listening', common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end(); diff --git a/test/parallel/test-http2-session-unref.js b/test/parallel/test-http2-session-unref.js new file mode 100644 index 00000000000000..e765352cdc615d --- /dev/null +++ b/test/parallel/test-http2-session-unref.js @@ -0,0 +1,53 @@ +'use strict'; +// Flags: --expose-internals + +// Tests that calling unref() on Http2Session: +// (1) Prevents it from keeping the process alive +// (2) Doesn't crash + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const makeDuplexPair = require('../common/duplexpair'); + +const server = http2.createServer(); +const { clientSide, serverSide } = makeDuplexPair(); + +// 'session' event should be emitted 3 times: +// - the vanilla client +// - the destroyed client +// - manual 'connection' event emission with generic Duplex stream +server.on('session', common.mustCallAtLeast((session) => { + session.unref(); +}, 3)); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + // unref new client + { + const client = http2.connect(`http://localhost:${port}`); + client.unref(); + } + + // unref destroyed client + { + const client = http2.connect(`http://localhost:${port}`); + client.destroy(); + client.unref(); + } + + // unref destroyed client + { + const client = http2.connect(`http://localhost:${port}`, { + createConnection: common.mustCall(() => clientSide) + }); + client.destroy(); + client.unref(); + } +})); +server.emit('connection', serverSide); +server.unref(); + +setTimeout(common.mustNotCall(() => {}), 1000).unref(); diff --git a/test/parallel/test-http2-settings-unsolicited-ack.js b/test/parallel/test-http2-settings-unsolicited-ack.js new file mode 100644 index 00000000000000..fa63e9ee3f6425 --- /dev/null +++ b/test/parallel/test-http2-settings-unsolicited-ack.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); +const net = require('net'); +const http2util = require('../common/http2'); +const Countdown = require('../common/countdown'); + +// Test that an unsolicited settings ack is ignored. + +const kSettings = new http2util.SettingsFrame(); +const kSettingsAck = new http2util.SettingsFrame(true); + +const server = http2.createServer(); +let client; + +const countdown = new Countdown(3, () => { + client.destroy(); + server.close(); +}); + +server.on('stream', common.mustNotCall()); +server.on('session', common.mustCall((session) => { + session.on('remoteSettings', common.mustCall(() => countdown.dec())); +})); + +server.listen(0, common.mustCall(() => { + client = net.connect(server.address().port); + + // Ensures that the clients settings frames are not sent until the + // servers are received, so that the first ack is actually expected. + client.once('data', (chunk) => { + // The very first chunk of data we get from the server should + // be a settings frame. + assert.deepStrictEqual(chunk.slice(0, 9), kSettings.data); + // The first ack is expected. + client.write(kSettingsAck.data, () => countdown.dec()); + // The second one is not and will be ignored. + client.write(kSettingsAck.data, () => countdown.dec()); + }); + + client.on('connect', common.mustCall(() => { + client.write(http2util.kClientMagic); + client.write(kSettings.data); + })); +})); diff --git a/test/parallel/test-http2-shutdown-errors.js b/test/parallel/test-http2-shutdown-errors.js deleted file mode 100644 index 30bdb7a986d898..00000000000000 --- a/test/parallel/test-http2-shutdown-errors.js +++ /dev/null @@ -1,75 +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 shutdown -// - should emit ERR_HTTP2_ERROR on session for all errors - -const tests = Object.getOwnPropertyNames(constants) - .filter((key) => ( - key.indexOf('NGHTTP2_ERR') === 0 - )) - .map((key) => ({ - ngError: constants[key], - error: { - code: 'ERR_HTTP2_ERROR', - type: Error, - message: nghttp2ErrorString(constants[key]) - } - })); - -let currentError; - -// mock submitGoaway because we only care about testing error handling -Http2Session.prototype.goaway = () => 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 session` - ); - - stream.session.once('error', errorMustCall); - stream.on('error', errorMustNotCall); - - stream.session.shutdown(); -}, 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-single-headers.js b/test/parallel/test-http2-single-headers.js index bb2f57cba1a939..c545b065015050 100644 --- a/test/parallel/test-http2-single-headers.js +++ b/test/parallel/test-http2-single-headers.js @@ -26,35 +26,26 @@ server.on('stream', common.mustNotCall()); server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); - let remaining = singles.length * 2; - function maybeClose() { - if (--remaining === 0) { - server.close(); - client.destroy(); - } - } - singles.forEach((i) => { - const req = client.request({ - [i]: 'abc', - [i.toUpperCase()]: 'xyz' - }); - req.on('error', common.expectsError({ - code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', - type: Error, - message: `Header field "${i}" must have only a single value` - })); - req.on('error', common.mustCall(maybeClose)); - - const req2 = client.request({ - [i]: ['abc', 'xyz'] - }); - req2.on('error', common.expectsError({ - code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', - type: Error, - message: `Header field "${i}" must have only a single value` - })); - req2.on('error', common.mustCall(maybeClose)); + common.expectsError( + () => client.request({ [i]: 'abc', [i.toUpperCase()]: 'xyz' }), + { + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: `Header field "${i}" must have only a single value` + } + ); + + common.expectsError( + () => client.request({ [i]: ['abc', 'xyz'] }), + { + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: `Header field "${i}" must have only a single value` + } + ); }); + server.close(); + client.close(); })); diff --git a/test/parallel/test-http2-socket-proxy.js b/test/parallel/test-http2-socket-proxy.js index 3b15c18628d218..2d90ef7e952a55 100644 --- a/test/parallel/test-http2-socket-proxy.js +++ b/test/parallel/test-http2-socket-proxy.js @@ -57,7 +57,7 @@ server.on('stream', common.mustCall(function(stream, headers) { assert.strictEqual(socket.writable, 0); assert.strictEqual(socket.readable, 0); - stream.session.destroy(); + stream.end(); socket.setTimeout = undefined; assert.strictEqual(session.setTimeout, undefined); @@ -71,18 +71,11 @@ server.listen(0, common.mustCall(function() { const port = server.address().port; const url = `http://localhost:${port}`; const client = h2.connect(url, common.mustCall(() => { - const headers = { - ':path': '/', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - const request = client.request(headers); + const request = client.request(); request.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); - request.end(); request.resume(); })); })); diff --git a/test/parallel/test-http2-status-code-invalid.js b/test/parallel/test-http2-status-code-invalid.js index 3a0d882dea19da..3337aad32d7f70 100644 --- a/test/parallel/test-http2-status-code-invalid.js +++ b/test/parallel/test-http2-status-code-invalid.js @@ -36,6 +36,6 @@ server.listen(0, common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); })); diff --git a/test/parallel/test-http2-status-code.js b/test/parallel/test-http2-status-code.js index e8f64da368a5f5..d3642b4ff0217f 100644 --- a/test/parallel/test-http2-status-code.js +++ b/test/parallel/test-http2-status-code.js @@ -22,7 +22,7 @@ server.listen(0, common.mustCall(() => { let remaining = codes.length; function maybeClose() { if (--remaining === 0) { - client.destroy(); + client.close(); server.close(); } } diff --git a/test/parallel/test-http2-stream-client.js b/test/parallel/test-http2-stream-client.js index aa722c5ff2b6d9..3e6c6b2a8a1b5e 100644 --- a/test/parallel/test-http2-stream-client.js +++ b/test/parallel/test-http2-stream-client.js @@ -22,7 +22,7 @@ server.listen(0, common.mustCall(() => { const req = client.request(); req.resume(); req.on('close', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-stream-destroy-event-order.js b/test/parallel/test-http2-stream-destroy-event-order.js index c375bd80b38e22..e2732830efbd8a 100644 --- a/test/parallel/test-http2-stream-destroy-event-order.js +++ b/test/parallel/test-http2-stream-destroy-event-order.js @@ -3,26 +3,27 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const assert = require('assert'); const http2 = require('http2'); let client; let req; const server = http2.createServer(); server.on('stream', common.mustCall((stream) => { - stream.on('error', common.mustCall(() => { - stream.on('close', common.mustCall((code) => { - assert.strictEqual(code, 2); - client.destroy(); + stream.on('close', common.mustCall(() => { + stream.on('error', common.mustCall(() => { server.close(); })); })); - req.rstStream(2); + req.close(2); })); server.listen(0, common.mustCall(() => { client = http2.connect(`http://localhost:${server.address().port}`); req = client.request(); req.resume(); - req.on('error', common.mustCall()); + req.on('close', common.mustCall(() => { + req.on('error', common.mustCall(() => { + client.close(); + })); + })); })); diff --git a/test/parallel/test-http2-timeouts.js b/test/parallel/test-http2-timeouts.js index e3163a3dc8d927..083dcaf40c10ad 100644 --- a/test/parallel/test-http2-timeouts.js +++ b/test/parallel/test-http2-timeouts.js @@ -51,7 +51,7 @@ server.on('listening', common.mustCall(() => { req.resume(); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end(); })); diff --git a/test/parallel/test-http2-too-large-headers.js b/test/parallel/test-http2-too-large-headers.js index f7ac25170b846f..7a7082736160f8 100644 --- a/test/parallel/test-http2-too-large-headers.js +++ b/test/parallel/test-http2-too-large-headers.js @@ -25,7 +25,7 @@ server.listen(0, common.mustCall(() => { req.on('close', common.mustCall((code) => { assert.strictEqual(code, NGHTTP2_ENHANCE_YOUR_CALM); server.close(); - client.destroy(); + client.close(); })); }); diff --git a/test/parallel/test-http2-too-many-headers.js b/test/parallel/test-http2-too-many-headers.js index eff0fa9c351c32..f05511cff657e0 100644 --- a/test/parallel/test-http2-too-many-headers.js +++ b/test/parallel/test-http2-too-many-headers.js @@ -28,7 +28,7 @@ server.listen(0, common.mustCall(() => { req.on('close', common.mustCall((code) => { assert.strictEqual(code, NGHTTP2_ENHANCE_YOUR_CALM); server.close(); - client.destroy(); + client.close(); })); })); diff --git a/test/parallel/test-http2-too-many-settings.js b/test/parallel/test-http2-too-many-settings.js index 9b685fa8ceb16f..0302fe623da07c 100644 --- a/test/parallel/test-http2-too-many-settings.js +++ b/test/parallel/test-http2-too-many-settings.js @@ -1,7 +1,7 @@ 'use strict'; // Tests that attempting to send too many non-acknowledged -// settings frames will result in a throw. +// settings frames will result in an error const common = require('../common'); if (!common.hasCrypto) @@ -9,53 +9,41 @@ if (!common.hasCrypto) const assert = require('assert'); const h2 = require('http2'); -const maxPendingAck = 2; -const server = h2.createServer({ maxPendingAck: maxPendingAck + 1 }); - -let clients = 2; +const maxOutstandingSettings = 2; function doTest(session) { - for (let n = 0; n < maxPendingAck; n++) - assert.doesNotThrow(() => session.settings({ enablePush: false })); - common.expectsError(() => session.settings({ enablePush: false }), - { - code: 'ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', - type: Error - }); + session.on('error', common.expectsError({ + code: 'ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + type: Error + })); + for (let n = 0; n < maxOutstandingSettings; n++) { + session.settings({ enablePush: false }); + assert.strictEqual(session.pendingSettingsAck, true); + } } -server.on('stream', common.mustNotCall()); - -server.once('session', common.mustCall((session) => doTest(session))); - -server.listen(0); - -const closeServer = common.mustCall(() => { - if (--clients === 0) - server.close(); -}, clients); - -server.on('listening', common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`, - { maxPendingAck: maxPendingAck + 1 }); - let remaining = maxPendingAck + 1; - - client.on('close', closeServer); - client.on('localSettings', common.mustCall(() => { - if (--remaining <= 0) { - client.destroy(); - } - }, maxPendingAck + 1)); - client.on('connect', common.mustCall(() => doTest(client))); -})); +{ + const server = h2.createServer({ maxOutstandingSettings }); + server.on('stream', common.mustNotCall()); + server.once('session', common.mustCall((session) => doTest(session))); + + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + // On some operating systems, an ECONNRESET error may be emitted. + // On others it won't be. Do not make this a mustCall + client.on('error', () => {}); + client.on('close', common.mustCall(() => server.close())); + })); +} -// Setting maxPendingAck to 0, defaults it to 1 -server.on('listening', common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`, - { maxPendingAck: 0 }); +{ + const server = h2.createServer(); + server.on('stream', common.mustNotCall()); - client.on('close', closeServer); - client.on('localSettings', common.mustCall(() => { - client.destroy(); + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`, + { maxOutstandingSettings }); + client.on('connect', () => doTest(client)); + client.on('close', () => server.close()); })); -})); +} diff --git a/test/parallel/test-http2-too-many-streams.js b/test/parallel/test-http2-too-many-streams.js new file mode 100644 index 00000000000000..a4a67befa0f50a --- /dev/null +++ b/test/parallel/test-http2-too-many-streams.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const Countdown = require('../common/countdown'); +const http2 = require('http2'); +const assert = require('assert'); + +// Test that the maxConcurrentStreams setting is strictly enforced + +const server = http2.createServer({ settings: { maxConcurrentStreams: 1 } }); + +let c = 0; + +server.on('stream', common.mustCall((stream) => { + // Because we only allow one open stream at a time, + // c should never be greater than 1. + assert.strictEqual(++c, 1); + stream.respond(); + // Force some asynchronos stuff. + setImmediate(() => { + stream.end('ok'); + assert.strictEqual(--c, 0); + }); +}, 3)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const countdown = new Countdown(3, common.mustCall(() => { + server.close(); + client.destroy(); + })); + + client.on('remoteSettings', common.mustCall(() => { + assert.strictEqual(client.remoteSettings.maxConcurrentStreams, 1); + + { + const req = client.request(); + req.resume(); + req.on('close', () => { + countdown.dec(); + + setImmediate(() => { + const req = client.request(); + req.resume(); + req.on('close', () => countdown.dec()); + }); + }); + } + + { + const req = client.request(); + req.resume(); + req.on('close', () => countdown.dec()); + } + })); +})); diff --git a/test/parallel/test-http2-trailers.js b/test/parallel/test-http2-trailers.js index 5e0db6a30c3db0..1ca5bdf70d05b0 100644 --- a/test/parallel/test-http2-trailers.js +++ b/test/parallel/test-http2-trailers.js @@ -45,7 +45,7 @@ server.on('listening', common.mustCall(function() { })); req.on('end', common.mustCall(() => { server.close(); - client.destroy(); + client.close(); })); req.end('data'); diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js index 14e4ba23d8fdca..6ab8bcff02866e 100644 --- a/test/parallel/test-http2-util-update-options-buffer.js +++ b/test/parallel/test-http2-util-update-options-buffer.js @@ -19,7 +19,9 @@ 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_MAX_OUTSTANDING_PINGS = 6; -const IDX_OPTIONS_FLAGS = 7; +const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7; +const IDX_OPTIONS_MAX_SESSION_MEMORY = 8; +const IDX_OPTIONS_FLAGS = 9; { updateOptionsBuffer({ @@ -29,7 +31,9 @@ const IDX_OPTIONS_FLAGS = 7; peerMaxConcurrentStreams: 4, paddingStrategy: 5, maxHeaderListPairs: 6, - maxOutstandingPings: 7 + maxOutstandingPings: 7, + maxOutstandingSettings: 8, + maxSessionMemory: 9 }); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1); @@ -39,6 +43,8 @@ const IDX_OPTIONS_FLAGS = 7; strictEqual(optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY], 5); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7); + strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS], 8); + strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY], 9); const flags = optionsBuffer[IDX_OPTIONS_FLAGS]; @@ -49,6 +55,7 @@ const IDX_OPTIONS_FLAGS = 7; ok(flags & (1 << IDX_OPTIONS_PADDING_STRATEGY)); ok(flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS)); ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)); + ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS)); } { diff --git a/test/parallel/test-http2-window-size.js b/test/parallel/test-http2-window-size.js index 381416c0d23cc6..3d1c14de847e48 100644 --- a/test/parallel/test-http2-window-size.js +++ b/test/parallel/test-http2-window-size.js @@ -67,7 +67,7 @@ function run(buffers, initialWindowSize) { const actualBuffer = Buffer.concat(responses); assert.strictEqual(Buffer.compare(actualBuffer, expectedBuffer), 0); // shut down - client.destroy(); + client.close(); server.close(() => { resolve(); }); diff --git a/test/parallel/test-http2-write-callbacks.js b/test/parallel/test-http2-write-callbacks.js index 44e33573a680b6..eca7f00ea7e292 100644 --- a/test/parallel/test-http2-write-callbacks.js +++ b/test/parallel/test-http2-write-callbacks.js @@ -31,7 +31,7 @@ server.listen(0, common.mustCall(() => { req.on('data', (chunk) => actual += chunk); req.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); req.on('close', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/parallel/test-http2-write-empty-string.js b/test/parallel/test-http2-write-empty-string.js index fea261917187da..6e6ce5254ddcfc 100644 --- a/test/parallel/test-http2-write-empty-string.js +++ b/test/parallel/test-http2-write-empty-string.js @@ -34,7 +34,7 @@ server.listen(0, common.mustCall(function() { req.on('end', common.mustCall(function() { assert.strictEqual('1\n2\n3\n', res); - client.destroy(); + client.close(); })); req.end(); diff --git a/test/parallel/test-http2-zero-length-write.js b/test/parallel/test-http2-zero-length-write.js index 899c28bace6f53..0b50715330a1c4 100644 --- a/test/parallel/test-http2-zero-length-write.js +++ b/test/parallel/test-http2-zero-length-write.js @@ -41,11 +41,12 @@ server.listen(0, common.mustCall(() => { let actual = ''; const req = client.request({ ':method': 'POST' }); req.on('response', common.mustCall()); + req.setEncoding('utf8'); req.on('data', (chunk) => actual += chunk); req.on('end', common.mustCall(() => { assert.strictEqual(actual, expect); server.close(); - client.destroy(); + client.close(); })); getSrc().pipe(req); })); diff --git a/test/parallel/test-performance-gc.js b/test/parallel/test-performance-gc.js index 89a9041c1c1159..ec0714ea50034d 100644 --- a/test/parallel/test-performance-gc.js +++ b/test/parallel/test-performance-gc.js @@ -51,3 +51,13 @@ const kinds = [ // Keep the event loop alive to witness the GC async callback happen. setImmediate(() => setImmediate(() => 0)); } + +// GC should not keep the event loop alive +{ + let didCall = false; + process.on('beforeExit', () => { + assert(!didCall); + didCall = true; + global.gc(); + }); +} diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status index cf4763b00129cc..b95db2a111ea67 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status @@ -11,6 +11,8 @@ test-inspector-async-call-stack : PASS, FLAKY test-inspector-bindings : PASS, FLAKY test-inspector-debug-end : PASS, FLAKY test-inspector-async-hook-setup-at-signal: PASS, FLAKY +test-http2-ping-flood : PASS, FLAKY +test-http2-settings-flood : PASS, FLAKY [$system==linux] diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index bd2b3254f06594..5a6d4e0758adc1 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -25,6 +25,7 @@ const fixtures = require('../common/fixtures'); delete providers.HTTP2SESSION; delete providers.HTTP2STREAM; delete providers.HTTP2PING; + delete providers.HTTP2SETTINGS; const obj_keys = Object.keys(providers); if (obj_keys.length > 0) diff --git a/test/sequential/test-http2-max-session-memory.js b/test/sequential/test-http2-max-session-memory.js new file mode 100644 index 00000000000000..e16000d1261ab0 --- /dev/null +++ b/test/sequential/test-http2-max-session-memory.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); + +// Test that maxSessionMemory Caps work + +const largeBuffer = Buffer.alloc(1e6); + +const server = http2.createServer({ maxSessionMemory: 1 }); + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end(largeBuffer); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + { + const req = client.request(); + + req.on('response', () => { + // This one should be rejected because the server is over budget + // on the current memory allocation + const req = client.request(); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 11' + })); + req.on('close', common.mustCall(() => { + server.close(); + client.destroy(); + })); + }); + + req.resume(); + req.on('close', common.mustCall()); + } +})); diff --git a/test/sequential/test-http2-ping-flood.js b/test/sequential/test-http2-ping-flood.js new file mode 100644 index 00000000000000..5b47d51be9c5a8 --- /dev/null +++ b/test/sequential/test-http2-ping-flood.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const net = require('net'); +const http2util = require('../common/http2'); + +// Test that ping flooding causes the session to be torn down + +const kSettings = new http2util.SettingsFrame(); +const kPing = new http2util.PingFrame(); + +const server = http2.createServer(); + +server.on('stream', common.mustNotCall()); +server.on('session', common.mustCall((session) => { + session.on('error', common.expectsError({ + code: 'ERR_HTTP2_ERROR', + message: + 'Flooding was detected in this HTTP/2 session, and it must be closed' + })); + session.on('close', common.mustCall(() => { + server.close(); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = net.connect(server.address().port); + + // nghttp2 uses a limit of 10000 items in it's outbound queue. + // If this number is exceeded, a flooding error is raised. Set + // this lim higher to account for the ones that nghttp2 is + // successfully able to respond to. + // TODO(jasnell): Unfortunately, this test is inherently flaky because + // it is entirely dependent on how quickly the server is able to handle + // the inbound frames and whether those just happen to overflow nghttp2's + // outbound queue. The threshold at which the flood error occurs can vary + // from one system to another, and from one test run to another. + client.on('connect', common.mustCall(() => { + client.write(http2util.kClientMagic, () => { + client.write(kSettings.data, () => { + for (let n = 0; n < 35000; n++) + client.write(kPing.data); + }); + }); + })); + + // An error event may or may not be emitted, depending on operating system + // and timing. We do not really care if one is emitted here or not, as the + // error on the server side is what we are testing for. Do not make this + // a common.mustCall() and there's no need to check the error details. + client.on('error', () => {}); +})); diff --git a/test/sequential/test-http2-session-timeout.js b/test/sequential/test-http2-session-timeout.js index 7a401e90ea4bbc..fce4570563c584 100644 --- a/test/sequential/test-http2-session-timeout.js +++ b/test/sequential/test-http2-session-timeout.js @@ -38,7 +38,7 @@ server.listen(0, common.mustCall(() => { setTimeout(() => makeReq(attempts - 1), callTimeout); } else { server.removeListener('timeout', mustNotCall); - client.destroy(); + client.close(); server.close(); } }); diff --git a/test/sequential/test-http2-settings-flood.js b/test/sequential/test-http2-settings-flood.js new file mode 100644 index 00000000000000..bad4cec9a8d509 --- /dev/null +++ b/test/sequential/test-http2-settings-flood.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const net = require('net'); +const http2util = require('../common/http2'); + +// Test that settings flooding causes the session to be torn down + +const kSettings = new http2util.SettingsFrame(); + +const server = http2.createServer(); + +server.on('stream', common.mustNotCall()); +server.on('session', common.mustCall((session) => { + session.on('error', common.expectsError({ + code: 'ERR_HTTP2_ERROR', + message: + 'Flooding was detected in this HTTP/2 session, and it must be closed' + })); + session.on('close', common.mustCall(() => { + server.close(); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = net.connect(server.address().port); + + // nghttp2 uses a limit of 10000 items in it's outbound queue. + // If this number is exceeded, a flooding error is raised. Set + // this lim higher to account for the ones that nghttp2 is + // successfully able to respond to. + // TODO(jasnell): Unfortunately, this test is inherently flaky because + // it is entirely dependent on how quickly the server is able to handle + // the inbound frames and whether those just happen to overflow nghttp2's + // outbound queue. The threshold at which the flood error occurs can vary + // from one system to another, and from one test run to another. + client.on('connect', common.mustCall(() => { + client.write(http2util.kClientMagic, () => { + for (let n = 0; n < 35000; n++) + client.write(kSettings.data); + }); + })); + + // An error event may or may not be emitted, depending on operating system + // and timing. We do not really care if one is emitted here or not, as the + // error on the server side is what we are testing for. Do not make this + // a common.mustCall() and there's no need to check the error details. + client.on('error', () => {}); +})); diff --git a/test/sequential/test-http2-timeout-large-write-file.js b/test/sequential/test-http2-timeout-large-write-file.js index 922e5f99668da2..e32f6037eef9e3 100644 --- a/test/sequential/test-http2-timeout-large-write-file.js +++ b/test/sequential/test-http2-timeout-large-write-file.js @@ -79,7 +79,7 @@ server.listen(0, common.mustCall(() => { } }, 1)); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); })); diff --git a/test/sequential/test-http2-timeout-large-write.js b/test/sequential/test-http2-timeout-large-write.js index f0a11b2e44469e..a15fb46af6d28a 100644 --- a/test/sequential/test-http2-timeout-large-write.js +++ b/test/sequential/test-http2-timeout-large-write.js @@ -78,7 +78,7 @@ server.listen(0, common.mustCall(() => { } }, 1)); req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); server.close(); })); }));