http2: small fixes to compatibility layer
Expand argument validation through compat API, adjust behaviour of response.end to not throw if stream already closed to match http1, adjust behaviour of writeContinue to not throw if stream already closed and other very small tweaks. Add tests for added and fixed behaviour. Add tests for edge case behaviours of setTimeout, createPushResponse, destroy, end and trailers. PR-URL: https://github.com/nodejs/node/pull/15473 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Minwoo Jung <jmwsoft@gmail.com>
This commit is contained in:
parent
e5c290bed9
commit
bc23681aa2
@ -92,7 +92,7 @@ function onStreamError(error) {
|
|||||||
// errors in compatibility mode are
|
// errors in compatibility mode are
|
||||||
// not forwarded to the request
|
// not forwarded to the request
|
||||||
// and response objects. However,
|
// and response objects. However,
|
||||||
// they are forwarded to 'clientError'
|
// they are forwarded to 'streamError'
|
||||||
// on the server by Http2Stream
|
// on the server by Http2Stream
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,9 +248,9 @@ class Http2ServerRequest extends Readable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(msecs, callback) {
|
setTimeout(msecs, callback) {
|
||||||
const stream = this[kStream];
|
if (this[kState].closed)
|
||||||
if (stream === undefined) return;
|
return;
|
||||||
stream.setTimeout(msecs, callback);
|
this[kStream].setTimeout(msecs, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
[kFinish](code) {
|
[kFinish](code) {
|
||||||
@ -445,7 +445,7 @@ class Http2ServerResponse extends Stream {
|
|||||||
|
|
||||||
if (stream === undefined) {
|
if (stream === undefined) {
|
||||||
const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
|
const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
|
||||||
if (cb)
|
if (typeof cb === 'function')
|
||||||
process.nextTick(cb, err);
|
process.nextTick(cb, err);
|
||||||
else
|
else
|
||||||
throw err;
|
throw err;
|
||||||
@ -461,12 +461,11 @@ class Http2ServerResponse extends Stream {
|
|||||||
if (typeof chunk === 'function') {
|
if (typeof chunk === 'function') {
|
||||||
cb = chunk;
|
cb = chunk;
|
||||||
chunk = null;
|
chunk = null;
|
||||||
encoding = 'utf8';
|
|
||||||
} else if (typeof encoding === 'function') {
|
} else if (typeof encoding === 'function') {
|
||||||
cb = encoding;
|
cb = encoding;
|
||||||
encoding = 'utf8';
|
encoding = 'utf8';
|
||||||
}
|
}
|
||||||
if (stream === undefined || stream.finished === true) {
|
if (this.finished === true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (chunk !== null && chunk !== undefined) {
|
if (chunk !== null && chunk !== undefined) {
|
||||||
@ -482,21 +481,21 @@ class Http2ServerResponse extends Stream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy(err) {
|
destroy(err) {
|
||||||
const stream = this[kStream];
|
if (this[kState].closed)
|
||||||
if (stream === undefined) {
|
|
||||||
// nothing to do, already closed
|
|
||||||
return;
|
return;
|
||||||
}
|
this[kStream].destroy(err);
|
||||||
stream.destroy(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(msecs, callback) {
|
setTimeout(msecs, callback) {
|
||||||
const stream = this[kStream];
|
const stream = this[kStream];
|
||||||
if (stream === undefined) return;
|
if (this[kState].closed)
|
||||||
|
return;
|
||||||
stream.setTimeout(msecs, callback);
|
stream.setTimeout(msecs, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPushResponse(headers, callback) {
|
createPushResponse(headers, callback) {
|
||||||
|
if (typeof callback !== 'function')
|
||||||
|
throw new errors.TypeError('ERR_INVALID_CALLBACK');
|
||||||
const stream = this[kStream];
|
const stream = this[kStream];
|
||||||
if (stream === undefined) {
|
if (stream === undefined) {
|
||||||
process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
|
process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
|
||||||
@ -513,12 +512,9 @@ class Http2ServerResponse extends Stream {
|
|||||||
if (stream !== undefined &&
|
if (stream !== undefined &&
|
||||||
stream.destroyed === false &&
|
stream.destroyed === false &&
|
||||||
stream.headersSent === false) {
|
stream.headersSent === false) {
|
||||||
options = options || Object.create(null);
|
|
||||||
const state = this[kState];
|
|
||||||
const headers = this[kHeaders];
|
const headers = this[kHeaders];
|
||||||
headers[HTTP2_HEADER_STATUS] = state.statusCode;
|
headers[HTTP2_HEADER_STATUS] = this[kState].statusCode;
|
||||||
if (stream.finished === true)
|
options = options || Object.create(null);
|
||||||
options.endStream = true;
|
|
||||||
options.getTrailers = (trailers) => {
|
options.getTrailers = (trailers) => {
|
||||||
Object.assign(trailers, this[kTrailers]);
|
Object.assign(trailers, this[kTrailers]);
|
||||||
};
|
};
|
||||||
@ -542,7 +538,11 @@ class Http2ServerResponse extends Stream {
|
|||||||
// TODO doesn't support callbacks
|
// TODO doesn't support callbacks
|
||||||
writeContinue() {
|
writeContinue() {
|
||||||
const stream = this[kStream];
|
const stream = this[kStream];
|
||||||
if (stream === undefined) return false;
|
if (stream === undefined ||
|
||||||
|
stream.headersSent === true ||
|
||||||
|
stream.destroyed === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this[kStream].additionalHeaders({
|
this[kStream].additionalHeaders({
|
||||||
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE
|
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE
|
||||||
});
|
});
|
||||||
|
@ -21,6 +21,11 @@ function handler(req, res) {
|
|||||||
'abcd': '1'
|
'abcd': '1'
|
||||||
});
|
});
|
||||||
res.end(testResBody);
|
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(
|
const server = http2.createServer(
|
||||||
|
@ -4,13 +4,25 @@
|
|||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
if (!common.hasCrypto)
|
if (!common.hasCrypto)
|
||||||
common.skip('missing crypto');
|
common.skip('missing crypto');
|
||||||
|
const assert = require('assert');
|
||||||
const http2 = require('http2');
|
const http2 = require('http2');
|
||||||
|
|
||||||
|
const msecs = common.platformTimeout(1);
|
||||||
const server = http2.createServer();
|
const server = http2.createServer();
|
||||||
|
|
||||||
server.on('request', (req, res) => {
|
server.on('request', (req, res) => {
|
||||||
req.setTimeout(common.platformTimeout(1), common.mustCall(() => {
|
req.setTimeout(msecs, common.mustCall(() => {
|
||||||
res.end();
|
res.end();
|
||||||
|
req.setTimeout(msecs, common.mustNotCall());
|
||||||
|
}));
|
||||||
|
res.on('finish', common.mustCall(() => {
|
||||||
|
req.setTimeout(msecs, common.mustNotCall());
|
||||||
|
process.nextTick(() => {
|
||||||
|
assert.doesNotThrow(
|
||||||
|
() => req.setTimeout(msecs, common.mustNotCall())
|
||||||
|
);
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -24,7 +36,6 @@ server.listen(0, common.mustCall(() => {
|
|||||||
':authority': `localhost:${port}`
|
':authority': `localhost:${port}`
|
||||||
});
|
});
|
||||||
req.on('end', common.mustCall(() => {
|
req.on('end', common.mustCall(() => {
|
||||||
server.close();
|
|
||||||
client.destroy();
|
client.destroy();
|
||||||
}));
|
}));
|
||||||
req.resume();
|
req.resume();
|
||||||
|
@ -34,7 +34,10 @@ server.listen(0, common.mustCall(function() {
|
|||||||
response.on('finish', common.mustCall(function() {
|
response.on('finish', common.mustCall(function() {
|
||||||
assert.strictEqual(request.closed, true);
|
assert.strictEqual(request.closed, true);
|
||||||
assert.strictEqual(request.code, h2.constants.NGHTTP2_NO_ERROR);
|
assert.strictEqual(request.code, h2.constants.NGHTTP2_NO_ERROR);
|
||||||
|
process.nextTick(() => {
|
||||||
|
assert.strictEqual(request.socket, undefined);
|
||||||
server.close();
|
server.close();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
response.end();
|
response.end();
|
||||||
}));
|
}));
|
||||||
|
@ -16,6 +16,19 @@ const server = h2.createServer((request, response) => {
|
|||||||
assert.strictEqual(response.stream.id % 2, 1);
|
assert.strictEqual(response.stream.id % 2, 1);
|
||||||
response.write(servExpect);
|
response.write(servExpect);
|
||||||
|
|
||||||
|
// callback must be specified (and be a function)
|
||||||
|
common.expectsError(
|
||||||
|
() => response.createPushResponse({
|
||||||
|
':path': '/pushed',
|
||||||
|
':method': 'GET'
|
||||||
|
}, undefined),
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_CALLBACK',
|
||||||
|
type: TypeError,
|
||||||
|
message: 'Callback must be a function'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
response.createPushResponse({
|
response.createPushResponse({
|
||||||
':path': '/pushed',
|
':path': '/pushed',
|
||||||
':method': 'GET'
|
':method': 'GET'
|
||||||
|
@ -24,6 +24,9 @@ const server = http2.createServer(common.mustCall((req, res) => {
|
|||||||
res.on('finish', common.mustCall(() => {
|
res.on('finish', common.mustCall(() => {
|
||||||
assert.doesNotThrow(() => res.destroy(nextError));
|
assert.doesNotThrow(() => res.destroy(nextError));
|
||||||
assert.strictEqual(res.closed, true);
|
assert.strictEqual(res.closed, true);
|
||||||
|
process.nextTick(() => {
|
||||||
|
assert.doesNotThrow(() => res.destroy(nextError));
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (req.url !== '/') {
|
if (req.url !== '/') {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
const { mustCall, mustNotCall, hasCrypto, skip } = require('../common');
|
const { mustCall, mustNotCall, hasCrypto, skip } = require('../common');
|
||||||
if (!hasCrypto)
|
if (!hasCrypto)
|
||||||
skip('missing crypto');
|
skip('missing crypto');
|
||||||
const { doesNotThrow, strictEqual } = require('assert');
|
const { strictEqual } = require('assert');
|
||||||
const {
|
const {
|
||||||
createServer,
|
createServer,
|
||||||
connect,
|
connect,
|
||||||
@ -15,18 +15,82 @@ const {
|
|||||||
} = require('http2');
|
} = require('http2');
|
||||||
|
|
||||||
{
|
{
|
||||||
// Http2ServerResponse.end callback is called only the first time,
|
// Http2ServerResponse.end accepts chunk, encoding, cb as args
|
||||||
// but may be invoked repeatedly without throwing errors.
|
// It may be invoked repeatedly without throwing errors
|
||||||
|
// but callback will only be called once
|
||||||
const server = createServer(mustCall((request, response) => {
|
const server = createServer(mustCall((request, response) => {
|
||||||
strictEqual(response.closed, false);
|
strictEqual(response.closed, false);
|
||||||
response.on('finish', mustCall(() => process.nextTick(
|
response.end('end', 'utf8', mustCall(() => {
|
||||||
mustCall(() => doesNotThrow(() => response.end('test', mustNotCall())))
|
strictEqual(response.closed, true);
|
||||||
)));
|
response.end(mustNotCall());
|
||||||
|
process.nextTick(() => {
|
||||||
|
response.end(mustNotCall());
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
response.end(mustNotCall());
|
||||||
|
}));
|
||||||
|
server.listen(0, mustCall(() => {
|
||||||
|
let data = '';
|
||||||
|
const { port } = server.address();
|
||||||
|
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.setEncoding('utf8');
|
||||||
|
request.on('data', (chunk) => (data += chunk));
|
||||||
|
request.on('end', mustCall(() => {
|
||||||
|
strictEqual(data, 'end');
|
||||||
|
client.destroy();
|
||||||
|
}));
|
||||||
|
request.end();
|
||||||
|
request.resume();
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Http2ServerResponse.end can omit encoding arg, sets it to utf-8
|
||||||
|
const server = createServer(mustCall((request, response) => {
|
||||||
|
response.end('test\uD83D\uDE00', mustCall(() => {
|
||||||
|
server.close();
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
server.listen(0, mustCall(() => {
|
||||||
|
let data = '';
|
||||||
|
const { port } = server.address();
|
||||||
|
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.setEncoding('utf8');
|
||||||
|
request.on('data', (chunk) => (data += chunk));
|
||||||
|
request.on('end', mustCall(() => {
|
||||||
|
strictEqual(data, 'test\uD83D\uDE00');
|
||||||
|
client.destroy();
|
||||||
|
}));
|
||||||
|
request.end();
|
||||||
|
request.resume();
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Http2ServerResponse.end can omit chunk & encoding args
|
||||||
|
const server = createServer(mustCall((request, response) => {
|
||||||
response.end(mustCall(() => {
|
response.end(mustCall(() => {
|
||||||
server.close();
|
server.close();
|
||||||
}));
|
}));
|
||||||
response.end(mustNotCall());
|
|
||||||
strictEqual(response.closed, true);
|
|
||||||
}));
|
}));
|
||||||
server.listen(0, mustCall(() => {
|
server.listen(0, mustCall(() => {
|
||||||
const { port } = server.address();
|
const { port } = server.address();
|
||||||
|
@ -103,6 +103,14 @@ server.listen(0, common.mustCall(function() {
|
|||||||
message: 'The "name" argument must be of type string'
|
message: 'The "name" argument must be of type string'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
common.expectsError(
|
||||||
|
() => response.setHeader(''),
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_HTTP_TOKEN',
|
||||||
|
type: TypeError,
|
||||||
|
message: 'Header name must be a valid HTTP token [""]'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
response.setHeader(real, expectedValue);
|
response.setHeader(real, expectedValue);
|
||||||
const expectedHeaderNames = [real];
|
const expectedHeaderNames = [real];
|
||||||
@ -122,7 +130,12 @@ server.listen(0, common.mustCall(function() {
|
|||||||
response.on('finish', common.mustCall(function() {
|
response.on('finish', common.mustCall(function() {
|
||||||
assert.strictEqual(response.code, h2.constants.NGHTTP2_NO_ERROR);
|
assert.strictEqual(response.code, h2.constants.NGHTTP2_NO_ERROR);
|
||||||
assert.strictEqual(response.headersSent, true);
|
assert.strictEqual(response.headersSent, true);
|
||||||
|
process.nextTick(() => {
|
||||||
|
// can access headersSent after stream is undefined
|
||||||
|
assert.strictEqual(response.stream, undefined);
|
||||||
|
assert.strictEqual(response.headersSent, true);
|
||||||
server.close();
|
server.close();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
response.end();
|
response.end();
|
||||||
}));
|
}));
|
||||||
|
@ -4,13 +4,25 @@
|
|||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
if (!common.hasCrypto)
|
if (!common.hasCrypto)
|
||||||
common.skip('missing crypto');
|
common.skip('missing crypto');
|
||||||
|
const assert = require('assert');
|
||||||
const http2 = require('http2');
|
const http2 = require('http2');
|
||||||
|
|
||||||
|
const msecs = common.platformTimeout(1);
|
||||||
const server = http2.createServer();
|
const server = http2.createServer();
|
||||||
|
|
||||||
server.on('request', (req, res) => {
|
server.on('request', (req, res) => {
|
||||||
res.setTimeout(common.platformTimeout(1), common.mustCall(() => {
|
res.setTimeout(msecs, common.mustCall(() => {
|
||||||
res.end();
|
res.end();
|
||||||
|
res.setTimeout(msecs, common.mustNotCall());
|
||||||
|
}));
|
||||||
|
res.on('finish', common.mustCall(() => {
|
||||||
|
res.setTimeout(msecs, common.mustNotCall());
|
||||||
|
process.nextTick(() => {
|
||||||
|
assert.doesNotThrow(
|
||||||
|
() => res.setTimeout(msecs, common.mustNotCall())
|
||||||
|
);
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -24,7 +36,6 @@ server.listen(0, common.mustCall(() => {
|
|||||||
':authority': `localhost:${port}`
|
':authority': `localhost:${port}`
|
||||||
});
|
});
|
||||||
req.on('end', common.mustCall(() => {
|
req.on('end', common.mustCall(() => {
|
||||||
server.close();
|
|
||||||
client.destroy();
|
client.destroy();
|
||||||
}));
|
}));
|
||||||
req.resume();
|
req.resume();
|
||||||
|
@ -14,6 +14,49 @@ server.listen(0, common.mustCall(() => {
|
|||||||
response.addTrailers({
|
response.addTrailers({
|
||||||
ABC: 123
|
ABC: 123
|
||||||
});
|
});
|
||||||
|
response.setTrailer('ABCD', 123);
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => response.addTrailers({ '': 'test' }),
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_HTTP_TOKEN',
|
||||||
|
type: TypeError,
|
||||||
|
message: 'Header name must be a valid HTTP token [""]'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
common.expectsError(
|
||||||
|
() => response.setTrailer('test', undefined),
|
||||||
|
{
|
||||||
|
code: 'ERR_HTTP2_INVALID_HEADER_VALUE',
|
||||||
|
type: TypeError,
|
||||||
|
message: 'Value must not be undefined or null'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
common.expectsError(
|
||||||
|
() => response.setTrailer('test', null),
|
||||||
|
{
|
||||||
|
code: 'ERR_HTTP2_INVALID_HEADER_VALUE',
|
||||||
|
type: TypeError,
|
||||||
|
message: 'Value must not be undefined or null'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
common.expectsError(
|
||||||
|
() => response.setTrailer(), // trailer name undefined
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
type: TypeError,
|
||||||
|
message: 'The "name" argument must be of type string'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
common.expectsError(
|
||||||
|
() => response.setTrailer(''),
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_HTTP_TOKEN',
|
||||||
|
type: TypeError,
|
||||||
|
message: 'Header name must be a valid HTTP token [""]'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
response.end('hello');
|
response.end('hello');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -22,6 +65,7 @@ server.listen(0, common.mustCall(() => {
|
|||||||
const request = client.request();
|
const request = client.request();
|
||||||
request.on('trailers', common.mustCall((headers) => {
|
request.on('trailers', common.mustCall((headers) => {
|
||||||
assert.strictEqual(headers.abc, '123');
|
assert.strictEqual(headers.abc, '123');
|
||||||
|
assert.strictEqual(headers.abcd, '123');
|
||||||
}));
|
}));
|
||||||
request.resume();
|
request.resume();
|
||||||
request.on('end', common.mustCall(() => {
|
request.on('end', common.mustCall(() => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user