Browse Source

http2: near full http1 compatibility, add tests

Extensive re-work of http1 compatibility layer based on tests in
express, on-finished and finalhandler. Fix handling of HEAD
method to match http1. Adjust write, end, etc. to call writeHead
as in http1 and as expected by user-land modules. Add socket
proxy that instead uses the Http2Stream for the vast majority of
socket interactions. Add and change tests to closer represent
http1 behaviour.

Refs: https://github.com/nodejs/node/pull/15633
Refs: https://github.com/expressjs/express/tree/master/test
Refs: https://github.com/jshttp/on-finished/blob/master/test/test.js
Refs: https://github.com/pillarjs/finalhandler/blob/master/test/test.js
PR-URL: https://github.com/nodejs/node/pull/15702
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
v9.x-staging
Anatoli Papirovski 8 years ago
committed by Matteo Collina
parent
commit
2da7d9b820
  1. 6
      doc/api/errors.md
  2. 97
      doc/api/http2.md
  3. 3
      lib/internal/errors.js
  4. 335
      lib/internal/http2/compat.js
  5. 16
      lib/internal/http2/core.js
  6. 9
      test/parallel/test-http2-compat-serverrequest-end.js
  7. 21
      test/parallel/test-http2-compat-serverrequest-headers.js
  8. 1
      test/parallel/test-http2-compat-serverrequest-pipe.js
  9. 7
      test/parallel/test-http2-compat-serverrequest.js
  10. 1
      test/parallel/test-http2-compat-serverresponse-destroy.js
  11. 191
      test/parallel/test-http2-compat-serverresponse-end.js
  12. 14
      test/parallel/test-http2-compat-serverresponse-finished.js
  13. 7
      test/parallel/test-http2-compat-serverresponse-flushheaders.js
  14. 47
      test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js
  15. 40
      test/parallel/test-http2-compat-serverresponse-headers.js
  16. 2
      test/parallel/test-http2-compat-serverresponse-writehead.js
  17. 107
      test/parallel/test-http2-compat-socket-set.js
  18. 89
      test/parallel/test-http2-compat-socket.js
  19. 9
      test/parallel/test-http2-options-max-reserved-streams.js
  20. 12
      test/parallel/test-http2-server-rst-stream.js
  21. 28
      test/parallel/test-http2-stream-destroy-event-order.js

6
doc/api/errors.md

@ -802,6 +802,12 @@ SETTINGS. By default, a maximum number of un-acknowledged `SETTINGS` frame may
be sent at any given time. This error code is used when that limit has been
reached.
<a id="ERR_HTTP2_NO_SOCKET_MANIPULATION"></a>
### ERR_HTTP2_NO_SOCKET_MANIPULATION
Used when attempting to read, write, pause, and/or resume a socket attached to
an `Http2Session`.
<a id="ERR_HTTP2_OUT_OF_STREAMS"></a>
### ERR_HTTP2_OUT_OF_STREAMS

97
doc/api/http2.md

@ -2046,7 +2046,7 @@ console.log(request.headers);
See [Headers Object][].
### request.httpVersion
#### request.httpVersion
<!-- YAML
added: v8.4.0
-->
@ -2117,7 +2117,14 @@ added: v8.4.0
* `msecs` {number}
* `callback` {Function}
Calls `request.connection.setTimeout(msecs, callback)`.
Sets the [`Http2Stream`]()'s timeout value to `msecs`. If a callback is
provided, then it is added as a listener on the `'timeout'` event on
the response object.
If no `'timeout'` listener is added to the request, the response, or
the server, then [`Http2Stream`]()s are destroyed when they time out. If a
handler is assigned to the request, the response, or the server's `'timeout'`
events, timed out sockets must be handled explicitly.
Returns `request`.
@ -2128,13 +2135,24 @@ added: v8.4.0
* {net.Socket}
The [`net.Socket`][] object associated with the connection.
Returns a Proxy object that acts as a `net.Socket` but applies getters,
setters and methods based on HTTP/2 logic.
`destroyed`, `readable`, and `writable` properties will be retrieved from and
set on `request.stream`.
With TLS support, use [`request.socket.getPeerCertificate()`][] to obtain the
client's authentication details.
`destroy`, `emit`, `end`, `on` and `once` methods will be called on
`request.stream`.
*Note*: do not use this socket object to send or receive any data. All
data transfers are managed by HTTP/2 and data might be lost.
`setTimeout` method will be called on `request.stream.session`.
`pause`, `read`, `resume`, and `write` will throw an error with code
`ERR_HTTP2_NO_SOCKET_MANIPULATION`. See [`Http2Session and Sockets`][] for
more information.
All other interactions will be routed directly to the socket. With TLS support,
use [`request.socket.getPeerCertificate()`][] to obtain the client's
authentication details.
#### request.stream
<!-- YAML
@ -2232,7 +2250,7 @@ passed as the second parameter to the [`'request'`][] event.
The response implements, but does not inherit from, the [Writable Stream][]
interface. This is an [`EventEmitter`][] with the following events:
### Event: 'close'
#### Event: 'close'
<!-- YAML
added: v8.4.0
-->
@ -2240,7 +2258,7 @@ added: v8.4.0
Indicates that the underlying [`Http2Stream`]() was terminated before
[`response.end()`][] was called or able to flush.
### Event: 'finish'
#### Event: 'finish'
<!-- YAML
added: v8.4.0
-->
@ -2252,7 +2270,7 @@ does not imply that the client has received anything yet.
After this event, no more events will be emitted on the response object.
### response.addTrailers(headers)
#### response.addTrailers(headers)
<!-- YAML
added: v8.4.0
-->
@ -2265,7 +2283,7 @@ message) to the response.
Attempting to set a header field name or value that contains invalid characters
will result in a [`TypeError`][] being thrown.
### response.connection
#### response.connection
<!-- YAML
added: v8.4.0
-->
@ -2274,7 +2292,7 @@ added: v8.4.0
See [`response.socket`][].
### response.end([data][, encoding][, callback])
#### response.end([data][, encoding][, callback])
<!-- YAML
added: v8.4.0
-->
@ -2293,7 +2311,7 @@ If `data` is specified, it is equivalent to calling
If `callback` is specified, it will be called when the response stream
is finished.
### response.finished
#### response.finished
<!-- YAML
added: v8.4.0
-->
@ -2303,7 +2321,7 @@ added: v8.4.0
Boolean value that indicates whether the response has completed. Starts
as `false`. After [`response.end()`][] executes, the value will be `true`.
### response.getHeader(name)
#### response.getHeader(name)
<!-- YAML
added: v8.4.0
-->
@ -2320,7 +2338,7 @@ Example:
const contentType = response.getHeader('content-type');
```
### response.getHeaderNames()
#### response.getHeaderNames()
<!-- YAML
added: v8.4.0
-->
@ -2340,7 +2358,7 @@ const headerNames = response.getHeaderNames();
// headerNames === ['foo', 'set-cookie']
```
### response.getHeaders()
#### response.getHeaders()
<!-- YAML
added: v8.4.0
-->
@ -2368,7 +2386,7 @@ const headers = response.getHeaders();
// headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] }
```
### response.hasHeader(name)
#### response.hasHeader(name)
<!-- YAML
added: v8.4.0
-->
@ -2385,7 +2403,7 @@ Example:
const hasContentType = response.hasHeader('content-type');
```
### response.headersSent
#### response.headersSent
<!-- YAML
added: v8.4.0
-->
@ -2394,7 +2412,7 @@ added: v8.4.0
Boolean (read-only). True if headers were sent, false otherwise.
### response.removeHeader(name)
#### response.removeHeader(name)
<!-- YAML
added: v8.4.0
-->
@ -2409,7 +2427,7 @@ Example:
response.removeHeader('Content-Encoding');
```
### response.sendDate
#### response.sendDate
<!-- YAML
added: v8.4.0
-->
@ -2422,7 +2440,7 @@ the response if it is not already present in the headers. Defaults to true.
This should only be disabled for testing; HTTP requires the Date header
in responses.
### response.setHeader(name, value)
#### response.setHeader(name, value)
<!-- YAML
added: v8.4.0
-->
@ -2463,7 +2481,7 @@ const server = http2.createServer((req, res) => {
});
```
### response.setTimeout(msecs[, callback])
#### response.setTimeout(msecs[, callback])
<!-- YAML
added: v8.4.0
-->
@ -2482,18 +2500,29 @@ events, timed out sockets must be handled explicitly.
Returns `response`.
### response.socket
#### response.socket
<!-- YAML
added: v8.4.0
-->
* {net.Socket}
Reference to the underlying socket. Usually users will not want to access
this property. In particular, the socket will not emit `'readable'` events
because of how the protocol parser attaches to the socket. After
`response.end()`, the property is nulled. The `socket` may also be accessed
via `response.connection`.
Returns a Proxy object that acts as a `net.Socket` but applies getters,
setters and methods based on HTTP/2 logic.
`destroyed`, `readable`, and `writable` properties will be retrieved from and
set on `response.stream`.
`destroy`, `emit`, `end`, `on` and `once` methods will be called on
`response.stream`.
`setTimeout` method will be called on `response.stream.session`.
`pause`, `read`, `resume`, and `write` will throw an error with code
`ERR_HTTP2_NO_SOCKET_MANIPULATION`. See [`Http2Session and Sockets`][] for
more information.
All other interactions will be routed directly to the socket.
Example:
@ -2506,7 +2535,7 @@ const server = http2.createServer((req, res) => {
}).listen(3000);
```
### response.statusCode
#### response.statusCode
<!-- YAML
added: v8.4.0
-->
@ -2526,7 +2555,7 @@ response.statusCode = 404;
After response header was sent to the client, this property indicates the
status code which was sent out.
### response.statusMessage
#### response.statusMessage
<!-- YAML
added: v8.4.0
-->
@ -2545,7 +2574,7 @@ added: v8.4.0
The [`Http2Stream`][] object backing the response.
### response.write(chunk[, encoding][, callback])
#### response.write(chunk[, encoding][, callback])
<!-- YAML
added: v8.4.0
-->
@ -2583,7 +2612,7 @@ Returns `true` if the entire data was flushed successfully to the kernel
buffer. Returns `false` if all or part of the data was queued in user memory.
`'drain'` will be emitted when the buffer is free again.
### response.writeContinue()
#### response.writeContinue()
<!-- YAML
added: v8.4.0
-->
@ -2592,7 +2621,7 @@ Sends a status `100 Continue` to the client, indicating that the request body
should be sent. See the [`'checkContinue'`][] event on `Http2Server` and
`Http2SecureServer`.
### response.writeHead(statusCode[, statusMessage][, headers])
#### response.writeHead(statusCode[, statusMessage][, headers])
<!-- YAML
added: v8.4.0
-->
@ -2648,7 +2677,7 @@ const server = http2.createServer((req, res) => {
Attempting to set a header field name or value that contains invalid characters
will result in a [`TypeError`][] being thrown.
### response.createPushResponse(headers, callback)
#### response.createPushResponse(headers, callback)
<!-- YAML
added: v8.4.0
-->

3
lib/internal/errors.js

@ -206,6 +206,9 @@ E('ERR_HTTP2_INVALID_SETTING_VALUE',
E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed');
E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
(max) => `Maximum number of pending settings acknowledgements (${max})`);
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
'HTTP/2 sockets should not be directly read from, written to, ' +
'paused and/or resumed.');
E('ERR_HTTP2_OUT_OF_STREAMS',
'No stream ID is available because maximum stream ID has been reached');
E('ERR_HTTP2_PAYLOAD_FORBIDDEN',

335
lib/internal/http2/compat.js

@ -16,10 +16,10 @@ const kHeaders = Symbol('headers');
const kRawHeaders = Symbol('rawHeaders');
const kTrailers = Symbol('trailers');
const kRawTrailers = Symbol('rawTrailers');
const kProxySocket = Symbol('proxySocket');
const kSetHeader = Symbol('setHeader');
const {
NGHTTP2_NO_ERROR,
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH,
@ -39,7 +39,7 @@ let statusMessageWarned = false;
// close as possible to the current require('http') API
function assertValidHeader(name, value) {
if (name === '')
if (name === '' || typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Header name', name);
if (isPseudoHeader(name))
throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED');
@ -71,19 +71,24 @@ function statusMessageWarn() {
}
function onStreamData(chunk) {
if (!this[kRequest].push(chunk))
const request = this[kRequest];
if (request !== undefined && !request.push(chunk))
this.pause();
}
function onStreamTrailers(trailers, flags, rawTrailers) {
const request = this[kRequest];
Object.assign(request[kTrailers], trailers);
request[kRawTrailers].push(...rawTrailers);
if (request !== undefined) {
Object.assign(request[kTrailers], trailers);
request[kRawTrailers].push(...rawTrailers);
}
}
function onStreamEnd() {
// Cause the request stream to end as well.
this[kRequest].push(null);
const request = this[kRequest];
if (request !== undefined)
this[kRequest].push(null);
}
function onStreamError(error) {
@ -97,62 +102,136 @@ function onStreamError(error) {
}
function onRequestPause() {
const stream = this[kStream];
if (stream)
stream.pause();
this[kStream].pause();
}
function onRequestResume() {
const stream = this[kStream];
if (stream)
stream.resume();
this[kStream].resume();
}
function onStreamDrain() {
this[kResponse].emit('drain');
const response = this[kResponse];
if (response !== undefined)
response.emit('drain');
}
// TODO Http2Stream does not emit 'close'
function onStreamClosedRequest() {
this[kRequest].push(null);
const request = this[kRequest];
if (request !== undefined)
request.push(null);
}
// TODO Http2Stream does not emit 'close'
function onStreamClosedResponse() {
this[kResponse].emit('finish');
const response = this[kResponse];
if (response !== undefined)
response.emit('finish');
}
function onStreamAbortedRequest(hadError, code) {
function onStreamAbortedRequest() {
const request = this[kRequest];
if (request[kState].closed === false) {
request.emit('aborted', hadError, code);
if (request !== undefined && request[kState].closed === false) {
request.emit('aborted');
request.emit('close');
}
}
function onStreamAbortedResponse() {
const response = this[kResponse];
if (response[kState].closed === false) {
if (response !== undefined && response[kState].closed === false)
response.emit('close');
}
}
function resumeStream(stream) {
stream.resume();
}
const proxySocketHandler = {
get(stream, prop) {
switch (prop) {
case 'on':
case 'once':
case 'end':
case 'emit':
case 'destroy':
return stream[prop].bind(stream);
case 'writable':
case 'destroyed':
return stream[prop];
case 'readable':
if (stream.destroyed)
return false;
const request = stream[kRequest];
return request ? request.readable : stream.readable;
case 'setTimeout':
const session = stream.session;
if (session !== undefined)
return session.setTimeout.bind(session);
return stream.setTimeout.bind(stream);
case 'write':
case 'read':
case 'pause':
case 'resume':
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
default:
const ref = stream.session !== undefined ?
stream.session.socket : stream;
const value = ref[prop];
return typeof value === 'function' ? value.bind(ref) : value;
}
},
getPrototypeOf(stream) {
if (stream.session !== undefined)
return stream.session.socket.constructor.prototype;
return stream.prototype;
},
set(stream, prop, value) {
switch (prop) {
case 'writable':
case 'readable':
case 'destroyed':
case 'on':
case 'once':
case 'end':
case 'emit':
case 'destroy':
stream[prop] = value;
return true;
case 'setTimeout':
const session = stream.session;
if (session !== undefined)
session[prop] = value;
else
stream[prop] = value;
return true;
case 'write':
case 'read':
case 'pause':
case 'resume':
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
default:
const ref = stream.session !== undefined ?
stream.session.socket : stream;
ref[prop] = value;
return true;
}
}
};
class Http2ServerRequest extends Readable {
constructor(stream, headers, options, rawHeaders) {
super(options);
this[kState] = {
closed: false,
closedCode: NGHTTP2_NO_ERROR
didRead: false,
};
this[kHeaders] = headers;
this[kRawHeaders] = rawHeaders;
this[kTrailers] = {};
this[kRawTrailers] = [];
this[kStream] = stream;
stream[kProxySocket] = null;
stream[kRequest] = this;
// Pause the stream..
@ -170,12 +249,10 @@ class Http2ServerRequest extends Readable {
this.on('resume', onRequestResume);
}
get closed() {
return this[kState].closed;
}
get code() {
return this[kState].closedCode;
get complete() {
return this._readableState.ended ||
this[kState].closed ||
this[kStream].destroyed;
}
get stream() {
@ -212,9 +289,10 @@ class Http2ServerRequest extends Readable {
get socket() {
const stream = this[kStream];
if (stream === undefined)
return;
return stream.session.socket;
const proxySocket = stream[kProxySocket];
if (proxySocket === null)
return stream[kProxySocket] = new Proxy(stream, proxySocketHandler);
return proxySocket;
}
get connection() {
@ -222,9 +300,10 @@ class Http2ServerRequest extends Readable {
}
_read(nread) {
const stream = this[kStream];
if (stream !== undefined) {
process.nextTick(resumeStream, stream);
const state = this[kState];
if (!state.closed) {
state.didRead = true;
process.nextTick(resumeStream, this[kStream]);
} else {
this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
}
@ -234,6 +313,13 @@ class Http2ServerRequest extends Readable {
return this[kHeaders][HTTP2_HEADER_METHOD];
}
set method(method) {
if (typeof method !== 'string' || method.trim() === '')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'method', 'string');
this[kHeaders][HTTP2_HEADER_METHOD] = method;
}
get authority() {
return this[kHeaders][HTTP2_HEADER_AUTHORITY];
}
@ -256,15 +342,17 @@ class Http2ServerRequest extends Readable {
this[kStream].setTimeout(msecs, callback);
}
[kFinish](code) {
[kFinish]() {
const state = this[kState];
if (state.closed)
return;
if (code !== undefined)
state.closedCode = Number(code);
state.closed = true;
this.push(null);
process.nextTick(() => (this[kStream] = undefined));
this[kStream][kRequest] = undefined;
// if the user didn't interact with incoming data and didn't pipe it,
// dump it for compatibility with http1
if (!state.didRead && !this._readableState.resumeScheduled)
this.resume();
}
}
@ -272,14 +360,16 @@ class Http2ServerResponse extends Stream {
constructor(stream, options) {
super(options);
this[kState] = {
closed: false,
ending: false,
headRequest: false,
sendDate: true,
statusCode: HTTP_STATUS_OK,
closed: false,
closedCode: NGHTTP2_NO_ERROR
};
this[kHeaders] = Object.create(null);
this[kTrailers] = Object.create(null);
this[kStream] = stream;
stream[kProxySocket] = null;
stream[kResponse] = this;
this.writable = true;
stream.on('drain', onStreamDrain);
@ -290,17 +380,35 @@ class Http2ServerResponse extends Stream {
stream.on('finish', onfinish);
}
// User land modules such as finalhandler just check truthiness of this
// but if someone is actually trying to use this for more than that
// then we simply can't support such use cases
get _header() {
return this.headersSent;
}
get finished() {
const stream = this[kStream];
return stream === undefined || stream._writableState.ended;
return stream.destroyed ||
stream._writableState.ended ||
this[kState].closed;
}
get closed() {
return this[kState].closed;
get socket() {
// this is compatible with http1 which removes socket reference
// only from ServerResponse but not IncomingMessage
if (this[kState].closed)
return;
const stream = this[kStream];
const proxySocket = stream[kProxySocket];
if (proxySocket === null)
return stream[kProxySocket] = new Proxy(stream, proxySocketHandler);
return proxySocket;
}
get code() {
return this[kState].closedCode;
get connection() {
return this.socket;
}
get stream() {
@ -308,8 +416,7 @@ class Http2ServerResponse extends Stream {
}
get headersSent() {
const stream = this[kStream];
return stream !== undefined ? stream.headersSent : this[kState].headersSent;
return this[kStream].headersSent;
}
get sendDate() {
@ -339,7 +446,7 @@ class Http2ServerResponse extends Stream {
name = name.trim().toLowerCase();
assertValidHeader(name, value);
this[kTrailers][name] = String(value);
this[kTrailers][name] = value;
}
addTrailers(headers) {
@ -379,6 +486,9 @@ class Http2ServerResponse extends Stream {
if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
if (this[kStream].headersSent)
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
name = name.trim().toLowerCase();
delete this[kHeaders][name];
}
@ -387,9 +497,16 @@ class Http2ServerResponse extends Stream {
if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
if (this[kStream].headersSent)
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
this[kSetHeader](name, value);
}
[kSetHeader](name, value) {
name = name.trim().toLowerCase();
assertValidHeader(name, value);
this[kHeaders][name] = String(value);
this[kHeaders][name] = value;
}
get statusMessage() {
@ -403,50 +520,45 @@ class Http2ServerResponse extends Stream {
}
flushHeaders() {
const stream = this[kStream];
if (stream !== undefined && stream.headersSent === false)
this[kBeginSend]();
const state = this[kState];
if (!state.closed && !this[kStream].headersSent)
this.writeHead(state.statusCode);
}
writeHead(statusCode, statusMessage, headers) {
if (typeof statusMessage === 'string') {
const state = this[kState];
if (state.closed)
throw new errors.Error('ERR_HTTP2_STREAM_CLOSED');
if (this[kStream].headersSent)
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
if (typeof statusMessage === 'string')
statusMessageWarn();
}
if (headers === undefined && typeof statusMessage === 'object') {
if (headers === undefined && typeof statusMessage === 'object')
headers = statusMessage;
}
const stream = this[kStream];
if (stream === undefined) {
throw new errors.Error('ERR_HTTP2_STREAM_CLOSED');
}
if (stream.headersSent === true) {
throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND');
}
if (typeof headers === 'object') {
const keys = Object.keys(headers);
let key = '';
for (var i = 0; i < keys.length; i++) {
key = keys[i];
this.setHeader(key, headers[key]);
this[kSetHeader](key, headers[key]);
}
}
this.statusCode = statusCode;
state.statusCode = statusCode;
this[kBeginSend]();
}
write(chunk, encoding, cb) {
const stream = this[kStream];
if (typeof encoding === 'function') {
cb = encoding;
encoding = 'utf8';
}
if (stream === undefined) {
if (this[kState].closed) {
const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
if (typeof cb === 'function')
process.nextTick(cb, err);
@ -454,12 +566,21 @@ class Http2ServerResponse extends Stream {
throw err;
return;
}
this[kBeginSend]();
const stream = this[kStream];
if (!stream.headersSent)
this.writeHead(this[kState].statusCode);
return stream.write(chunk, encoding, cb);
}
end(chunk, encoding, cb) {
const stream = this[kStream];
const state = this[kState];
if ((state.closed || state.ending) &&
state.headRequest === stream.headRequest) {
return false;
}
if (typeof chunk === 'function') {
cb = chunk;
@ -468,19 +589,28 @@ class Http2ServerResponse extends Stream {
cb = encoding;
encoding = 'utf8';
}
if (this.finished === true) {
return false;
}
if (chunk !== null && chunk !== undefined) {
if (chunk !== null && chunk !== undefined)
this.write(chunk, encoding);
}
const isFinished = this.finished;
state.headRequest = stream.headRequest;
state.ending = true;
if (typeof cb === 'function') {
stream.once('finish', cb);
if (isFinished)
this.once('finish', cb);
else
stream.once('finish', cb);
}
this[kBeginSend]({ endStream: true });
stream.end();
if (!stream.headersSent)
this.writeHead(this[kState].statusCode);
if (isFinished)
this[kFinish]();
else
stream.end();
}
destroy(err) {
@ -490,63 +620,52 @@ class Http2ServerResponse extends Stream {
}
setTimeout(msecs, callback) {
const stream = this[kStream];
if (this[kState].closed)
return;
stream.setTimeout(msecs, callback);
this[kStream].setTimeout(msecs, callback);
}
createPushResponse(headers, callback) {
if (typeof callback !== 'function')
throw new errors.TypeError('ERR_INVALID_CALLBACK');
const stream = this[kStream];
if (stream === undefined) {
if (this[kState].closed) {
process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
return;
}
stream.pushStream(headers, {}, function(stream, headers, options) {
this[kStream].pushStream(headers, {}, function(stream, headers, options) {
const response = new Http2ServerResponse(stream);
callback(null, response);
});
}
[kBeginSend](options) {
const stream = this[kStream];
if (stream !== undefined &&
stream.destroyed === false &&
stream.headersSent === false) {
const headers = this[kHeaders];
headers[HTTP2_HEADER_STATUS] = this[kState].statusCode;
options = options || Object.create(null);
options.getTrailers = (trailers) => {
Object.assign(trailers, this[kTrailers]);
};
stream.respond(headers, options);
}
[kBeginSend]() {
const state = this[kState];
const headers = this[kHeaders];
headers[HTTP2_HEADER_STATUS] = state.statusCode;
const options = {
endStream: state.ending,
getTrailers: (trailers) => Object.assign(trailers, this[kTrailers])
};
this[kStream].respond(headers, options);
}
[kFinish](code) {
[kFinish]() {
const stream = this[kStream];
const state = this[kState];
if (state.closed)
if (state.closed || stream.headRequest !== state.headRequest)
return;
if (code !== undefined)
state.closedCode = Number(code);
state.closed = true;
state.headersSent = this[kStream].headersSent;
this.end();
process.nextTick(() => (this[kStream] = undefined));
this[kProxySocket] = null;
stream[kResponse] = undefined;
this.emit('finish');
}
// TODO doesn't support callbacks
writeContinue() {
const stream = this[kStream];
if (stream === undefined ||
stream.headersSent === true ||
stream.destroyed === true) {
if (stream.headersSent || this[kState].closed)
return false;
}
this[kStream].additionalHeaders({
stream.additionalHeaders({
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE
});
return true;

16
lib/internal/http2/core.js

@ -166,8 +166,7 @@ function onSessionHeaders(id, cat, flags, headers) {
// For head requests, there must not be a body...
// end the writable side immediately.
stream.end();
const state = stream[kState];
state.headRequest = true;
stream[kState].headRequest = true;
}
} else {
stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream });
@ -1277,6 +1276,7 @@ class Http2Stream extends Duplex {
rst: false,
rstCode: NGHTTP2_NO_ERROR,
headersSent: false,
headRequest: false,
aborted: false,
closeHandler: onSessionClose.bind(this)
};
@ -1333,6 +1333,11 @@ class Http2Stream extends Duplex {
return this[kState].aborted;
}
// true if dealing with a HEAD request
get headRequest() {
return this[kState].headRequest;
}
// The error code reported when this Http2Stream was closed.
get rstCode() {
return this[kState].rst ? this[kState].rstCode : undefined;
@ -1533,12 +1538,15 @@ function continueStreamDestroy(self, err, callback) {
// All done
const rst = state.rst;
const code = rst ? state.rstCode : NGHTTP2_NO_ERROR;
if (!err && code !== NGHTTP2_NO_ERROR) {
// RST code 8 not emitted as an error as its used by clients to signify
// abort and is already covered by aborted event, also allows more
// seamless compatibility with http1
if (!err && code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL) {
err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
}
callback(err);
process.nextTick(emit, self, 'streamClosed', code);
debug(`[${sessionName(session[kType])}] stream ${self[kID]} destroyed`);
callback(err);
}
function finishStreamDestroy(self, handle) {

9
test/parallel/test-http2-compat-serverrequest-end.js

@ -3,6 +3,7 @@
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Http2ServerRequest should always end readable stream
@ -12,9 +13,17 @@ const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
assert.strictEqual(request.complete, false);
request.on('data', () => {});
request.on('end', common.mustCall(() => {
assert.strictEqual(request.complete, true);
response.on('finish', common.mustCall(function() {
// the following tests edge cases on request socket
// right after finished fires but before backing
// Http2Stream is destroyed
assert.strictEqual(request.socket.readable, request.stream.readable);
assert.strictEqual(request.socket.readable, false);
server.close();
}));
response.end();

21
test/parallel/test-http2-compat-serverrequest-headers.js

@ -41,6 +41,27 @@ server.listen(0, common.mustCall(function() {
request.url = '/one';
assert.strictEqual(request.url, '/one');
// third-party plugins for packages like express use query params to
// change the request method
request.method = 'POST';
assert.strictEqual(request.method, 'POST');
common.expectsError(
() => request.method = ' ',
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "method" argument must be of type string'
}
);
common.expectsError(
() => request.method = true,
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "method" argument must be of type string'
}
);
response.on('finish', common.mustCall(function() {
server.close();
}));

1
test/parallel/test-http2-compat-serverrequest-pipe.js

@ -19,6 +19,7 @@ const server = http2.createServer();
server.on('request', common.mustCall((req, res) => {
const dest = req.pipe(fs.createWriteStream(fn));
dest.on('finish', common.mustCall(() => {
assert.strictEqual(req.complete, true);
assert.deepStrictEqual(fs.readFileSync(loc), fs.readFileSync(fn));
fs.unlinkSync(fn);
res.end();

7
test/parallel/test-http2-compat-serverrequest.js

@ -19,9 +19,6 @@ server.listen(0, common.mustCall(function() {
httpVersionMinor: 0
};
assert.strictEqual(request.closed, false);
assert.strictEqual(request.code, h2.constants.NGHTTP2_NO_ERROR);
assert.strictEqual(request.httpVersion, expected.version);
assert.strictEqual(request.httpVersionMajor, expected.httpVersionMajor);
assert.strictEqual(request.httpVersionMinor, expected.httpVersionMinor);
@ -31,10 +28,8 @@ server.listen(0, common.mustCall(function() {
assert.strictEqual(request.socket, request.connection);
response.on('finish', common.mustCall(function() {
assert.strictEqual(request.closed, true);
assert.strictEqual(request.code, h2.constants.NGHTTP2_NO_ERROR);
process.nextTick(() => {
assert.strictEqual(request.socket, undefined);
assert.ok(request.socket);
server.close();
});
}));

1
test/parallel/test-http2-compat-serverresponse-destroy.js

@ -22,7 +22,6 @@ const server = http2.createServer(common.mustCall((req, res) => {
res.on('finish', common.mustCall(() => {
assert.doesNotThrow(() => res.destroy(nextError));
assert.strictEqual(res.closed, true);
process.nextTick(() => {
assert.doesNotThrow(() => res.destroy(nextError));
});

191
test/parallel/test-http2-compat-serverresponse-end.js

@ -1,6 +1,12 @@
'use strict';
const { mustCall, mustNotCall, hasCrypto, skip } = require('../common');
const {
mustCall,
mustNotCall,
hasCrypto,
platformTimeout,
skip
} = require('../common');
if (!hasCrypto)
skip('missing crypto');
const { strictEqual } = require('assert');
@ -18,15 +24,16 @@ const {
// It may be invoked repeatedly without throwing errors
// but callback will only be called once
const server = createServer(mustCall((request, response) => {
strictEqual(response.closed, false);
response.end('end', 'utf8', mustCall(() => {
strictEqual(response.closed, true);
response.end(mustNotCall());
process.nextTick(() => {
response.end(mustNotCall());
server.close();
});
}));
response.on('finish', mustCall(() => {
response.end(mustNotCall());
}));
response.end(mustNotCall());
}));
server.listen(0, mustCall(() => {
@ -111,12 +118,77 @@ const {
}
{
// Http2ServerResponse.end is not necessary on HEAD requests since the stream
// is already closed. Headers, however, can still be sent to the client.
// Http2ServerResponse.end is necessary on HEAD requests in compat
// for http1 compatibility
const server = createServer(mustCall((request, response) => {
strictEqual(response.finished, true);
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.end(mustNotCall());
response.end('data', mustCall());
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// .end should trigger 'end' event on request if user did not attempt
// to read from the request
const server = createServer(mustCall((request, response) => {
request.on('end', mustCall());
response.end();
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to call .end with cb from stream 'streamClosed'
const server = createServer(mustCall((request, response) => {
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.stream.on('streamClosed', mustCall(() => {
response.end(mustCall());
}));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
@ -144,3 +216,110 @@ const {
}));
}));
}
{
// Should be able to respond to HEAD request after timeout
const server = createServer(mustCall((request, response) => {
setTimeout(mustCall(() => {
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.end('data', mustCall());
}), platformTimeout(10));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// finish should only trigger after 'end' is called
const server = createServer(mustCall((request, response) => {
let finished = false;
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.on('finish', mustCall(() => {
finished = false;
}));
response.end('data', mustCall(() => {
strictEqual(finished, false);
response.end('data', mustNotCall());
}));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to respond to HEAD with just .end
const server = createServer(mustCall((request, response) => {
response.end('data', mustCall());
response.end(mustNotCall());
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}

14
test/parallel/test-http2-compat-serverresponse-finished.js

@ -5,19 +5,23 @@ if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
const net = require('net');
// Http2ServerResponse.finished
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
assert.ok(response.socket instanceof net.Socket);
assert.ok(response.connection instanceof net.Socket);
assert.strictEqual(response.socket, response.connection);
response.on('finish', common.mustCall(function() {
assert.ok(request.stream !== undefined);
assert.ok(response.stream !== undefined);
server.close();
assert.strictEqual(response.socket, undefined);
assert.strictEqual(response.connection, undefined);
process.nextTick(common.mustCall(() => {
assert.strictEqual(request.stream, undefined);
assert.strictEqual(response.stream, undefined);
assert.ok(response.stream);
server.close();
}));
}));
assert.strictEqual(response.finished, false);

7
test/parallel/test-http2-compat-serverresponse-flushheaders.js

@ -15,18 +15,23 @@ server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
assert.strictEqual(response.headersSent, false);
assert.strictEqual(response._header, false); // alias for headersSent
response.flushHeaders();
assert.strictEqual(response.headersSent, true);
assert.strictEqual(response._header, true);
response.flushHeaders(); // Idempotent
common.expectsError(() => {
response.writeHead(400, { 'foo-bar': 'abc123' });
}, {
code: 'ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND'
code: 'ERR_HTTP2_HEADERS_SENT'
});
response.on('finish', common.mustCall(function() {
server.close();
process.nextTick(() => {
response.flushHeaders(); // Idempotent
});
}));
serverResponse = response;
}));

47
test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js

@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// makes sure that Http2ServerResponse setHeader & removeHeader, do not throw
// any errors if the stream was destroyed before headers were sent
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'));
assert.doesNotThrow(() => response.removeHeader('test', 'value'));
process.nextTick(() => {
assert.doesNotThrow(() => response.setHeader('test', 'value'));
assert.doesNotThrow(() => response.removeHeader('test', 'value'));
server.close();
});
}));
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(function() {
client.destroy();
}));
request.end();
request.resume();
}));
}));

40
test/parallel/test-http2-compat-serverresponse-headers.js

@ -124,14 +124,44 @@ server.listen(0, common.mustCall(function() {
response.sendDate = false;
assert.strictEqual(response.sendDate, false);
assert.strictEqual(response.code, h2.constants.NGHTTP2_NO_ERROR);
response.on('finish', common.mustCall(function() {
assert.strictEqual(response.code, h2.constants.NGHTTP2_NO_ERROR);
assert.strictEqual(response.headersSent, true);
common.expectsError(
() => response.setHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
common.expectsError(
() => response.removeHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
process.nextTick(() => {
// can access headersSent after stream is undefined
assert.strictEqual(response.stream, undefined);
common.expectsError(
() => response.setHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
common.expectsError(
() => response.removeHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
assert.strictEqual(response.headersSent, true);
server.close();
});

2
test/parallel/test-http2-compat-serverresponse-writehead.js

@ -16,7 +16,7 @@ server.listen(0, common.mustCall(function() {
response.writeHead(418, { 'foo-bar': 'abc123' }); // Override
common.expectsError(() => { response.writeHead(300); }, {
code: 'ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND'
code: 'ERR_HTTP2_HEADERS_SENT'
});
response.on('finish', common.mustCall(function() {

107
test/parallel/test-http2-compat-socket-set.js

@ -0,0 +1,107 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Tests behaviour of the proxied socket in Http2ServerRequest
// & Http2ServerResponse - specifically property setters
const errMsg = {
code: 'ERR_HTTP2_NO_SOCKET_MANIPULATION',
type: Error,
message: 'HTTP/2 sockets should not be directly read from, written to, ' +
'paused and/or resumed.'
};
const server = h2.createServer();
server.on('request', common.mustCall(function(request, response) {
const noop = () => {};
assert.strictEqual(request.stream.destroyed, false);
request.socket.destroyed = true;
assert.strictEqual(request.stream.destroyed, true);
request.socket.destroyed = false;
assert.strictEqual(request.stream.readable, false);
request.socket.readable = true;
assert.strictEqual(request.stream.readable, true);
assert.strictEqual(request.stream.writable, true);
request.socket.writable = false;
assert.strictEqual(request.stream.writable, false);
const realOn = request.stream.on;
request.socket.on = noop;
assert.strictEqual(request.stream.on, noop);
request.stream.on = realOn;
const realOnce = request.stream.once;
request.socket.once = noop;
assert.strictEqual(request.stream.once, noop);
request.stream.once = realOnce;
const realEnd = request.stream.end;
request.socket.end = noop;
assert.strictEqual(request.stream.end, noop);
request.socket.end = common.mustCall();
request.socket.end();
request.stream.end = realEnd;
const realEmit = request.stream.emit;
request.socket.emit = noop;
assert.strictEqual(request.stream.emit, noop);
request.stream.emit = realEmit;
const realDestroy = request.stream.destroy;
request.socket.destroy = noop;
assert.strictEqual(request.stream.destroy, noop);
request.stream.destroy = realDestroy;
request.socket.setTimeout = noop;
assert.strictEqual(request.stream.session.setTimeout, noop);
assert.strictEqual(request.stream.session.socket._isProcessing, undefined);
request.socket._isProcessing = true;
assert.strictEqual(request.stream.session.socket._isProcessing, true);
common.expectsError(() => request.socket.read = noop, errMsg);
common.expectsError(() => request.socket.write = noop, errMsg);
common.expectsError(() => request.socket.pause = noop, errMsg);
common.expectsError(() => request.socket.resume = noop, errMsg);
request.stream.on('finish', common.mustCall(() => {
setImmediate(() => {
request.socket.setTimeout = noop;
assert.strictEqual(request.stream.setTimeout, noop);
assert.strictEqual(request.stream._isProcessing, undefined);
request.socket._isProcessing = true;
assert.strictEqual(request.stream._isProcessing, true);
});
}));
response.stream.destroy();
}));
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': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));

89
test/parallel/test-http2-compat-socket.js

@ -0,0 +1,89 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
const net = require('net');
// Tests behaviour of the proxied socket in Http2ServerRequest
// & Http2ServerResponse - this proxy socket should mimic the
// behaviour of http1 but against the http2 api & model
const errMsg = {
code: 'ERR_HTTP2_NO_SOCKET_MANIPULATION',
type: Error,
message: 'HTTP/2 sockets should not be directly read from, written to, ' +
'paused and/or resumed.'
};
const server = h2.createServer();
server.on('request', common.mustCall(function(request, response) {
assert.ok(request.socket instanceof net.Socket);
assert.ok(response.socket instanceof net.Socket);
assert.strictEqual(request.socket, response.socket);
assert.ok(request.socket.readable);
request.resume();
assert.ok(request.socket.writable);
assert.strictEqual(request.socket.destroyed, false);
request.socket.setTimeout(987);
assert.strictEqual(request.stream.session._idleTimeout, 987);
request.socket.setTimeout(0);
common.expectsError(() => request.socket.read(), errMsg);
common.expectsError(() => request.socket.write(), errMsg);
common.expectsError(() => request.socket.pause(), errMsg);
common.expectsError(() => request.socket.resume(), errMsg);
// should have correct this context for socket methods & getters
assert.ok(request.socket.address() != null);
assert.ok(request.socket.remotePort);
request.on('end', common.mustCall(() => {
assert.strictEqual(request.socket.readable, false);
assert.doesNotThrow(() => response.socket.destroy());
}));
response.on('finish', common.mustCall(() => {
assert.ok(request.socket);
assert.strictEqual(response.socket, undefined);
assert.ok(request.socket.destroyed);
assert.strictEqual(request.socket.readable, false);
process.nextTick(() => {
assert.strictEqual(request.socket.writable, false);
server.close();
});
}));
// properties that do not exist on the proxy are retrieved from the socket
assert.ok(request.socket._server);
assert.strictEqual(request.socket.connecting, false);
// socket events are bound and emitted on Http2Stream
request.socket.on('streamClosed', common.mustCall());
request.socket.once('streamClosed', common.mustCall());
request.socket.on('testEvent', common.mustCall());
request.socket.emit('testEvent');
}));
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);
request.on('end', common.mustCall(() => {
client.destroy();
}));
request.end();
request.resume();
}));
}));

9
test/parallel/test-http2-options-max-reserved-streams.js

@ -3,6 +3,7 @@
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
const server = h2.createServer();
@ -32,11 +33,9 @@ server.on('stream', common.mustCall((stream) => {
}, common.mustCall((pushedStream) => {
pushedStream.respond({ ':status': 200 });
pushedStream.on('aborted', common.mustCall());
pushedStream.on('error', common.mustCall(common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR',
type: Error,
message: 'Stream closed with error code 8'
})));
pushedStream.on('error', common.mustNotCall());
pushedStream.on('streamClosed',
common.mustCall((code) => assert.strictEqual(code, 8)));
}));
stream.end('hello world');

12
test/parallel/test-http2-server-rst-stream.js

@ -17,7 +17,7 @@ const {
NGHTTP2_INTERNAL_ERROR
} = http2.constants;
const errCheck = common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR' }, 8);
const errCheck = common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR' }, 6);
function checkRstCode(rstMethod, expectRstCode) {
const server = http2.createServer();
@ -32,8 +32,11 @@ function checkRstCode(rstMethod, expectRstCode) {
else
stream[rstMethod]();
if (expectRstCode > NGHTTP2_NO_ERROR) {
if (expectRstCode !== NGHTTP2_NO_ERROR &&
expectRstCode !== NGHTTP2_CANCEL) {
stream.on('error', common.mustCall(errCheck));
} else {
stream.on('error', common.mustNotCall());
}
});
@ -58,8 +61,11 @@ function checkRstCode(rstMethod, expectRstCode) {
req.on('aborted', common.mustCall());
req.on('end', common.mustCall());
if (expectRstCode > NGHTTP2_NO_ERROR) {
if (expectRstCode !== NGHTTP2_NO_ERROR &&
expectRstCode !== NGHTTP2_CANCEL) {
req.on('error', common.mustCall(errCheck));
} else {
req.on('error', common.mustNotCall());
}
}));

28
test/parallel/test-http2-stream-destroy-event-order.js

@ -0,0 +1,28 @@
'use strict';
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('streamClosed', common.mustCall((code) => {
assert.strictEqual(code, 2);
client.destroy();
server.close();
}));
}));
req.rstStream(2);
}));
server.listen(0, common.mustCall(() => {
client = http2.connect(`http://localhost:${server.address().port}`);
req = client.request();
req.resume();
req.on('error', common.mustCall());
}));
Loading…
Cancel
Save