diff --git a/benchmark/README.md b/benchmark/README.md
index 17c733e6eb0b6c..dfdf319b9cb311 100644
--- a/benchmark/README.md
+++ b/benchmark/README.md
@@ -97,6 +97,12 @@ directory, see [the guide on benchmarks](../doc/guides/writing-and-running-bench
Benchmarks for the http
subsystem.
+
misc |
diff --git a/benchmark/_http-benchmarkers.js b/benchmark/_http-benchmarkers.js
index 3f17f05f831170..f9359b13d5e9e4 100644
--- a/benchmark/_http-benchmarkers.js
+++ b/benchmark/_http-benchmarkers.js
@@ -111,10 +111,63 @@ class TestDoubleBenchmarker {
}
}
+/**
+ * HTTP/2 Benchmarker
+ */
+class H2LoadBenchmarker {
+ constructor() {
+ this.name = 'h2load';
+ this.executable = 'h2load';
+ const result = child_process.spawnSync(this.executable, ['-h']);
+ this.present = !(result.error && result.error.code === 'ENOENT');
+ }
+
+ create(options) {
+ const args = [];
+ if (typeof options.requests === 'number')
+ args.push('-n', options.requests);
+ if (typeof options.clients === 'number')
+ args.push('-c', options.clients);
+ if (typeof options.threads === 'number')
+ args.push('-t', options.threads);
+ if (typeof options.maxConcurrentStreams === 'number')
+ args.push('-m', options.maxConcurrentStreams);
+ if (typeof options.initialWindowSize === 'number')
+ args.push('-w', options.initialWindowSize);
+ if (typeof options.sessionInitialWindowSize === 'number')
+ args.push('-W', options.sessionInitialWindowSize);
+ if (typeof options.rate === 'number')
+ args.push('-r', options.rate);
+ if (typeof options.ratePeriod === 'number')
+ args.push(`--rate-period=${options.ratePeriod}`);
+ if (typeof options.duration === 'number')
+ args.push('-T', options.duration);
+ if (typeof options.timeout === 'number')
+ args.push('-N', options.timeout);
+ if (typeof options.headerTableSize === 'number')
+ args.push(`--header-table-size=${options.headerTableSize}`);
+ if (typeof options.encoderHeaderTableSize === 'number') {
+ args.push(
+ `--encoder-header-table-size=${options.encoderHeaderTableSize}`);
+ }
+ const scheme = options.scheme || 'http';
+ const host = options.host || '127.0.0.1';
+ args.push(`${scheme}://${host}:${options.port}${options.path}`);
+ const child = child_process.spawn(this.executable, args);
+ return child;
+ }
+
+ processResults(output) {
+ const rex = /(\d+(?:\.\d+)) req\/s/;
+ return rex.exec(output)[1];
+ }
+}
+
const http_benchmarkers = [
new WrkBenchmarker(),
new AutocannonBenchmarker(),
- new TestDoubleBenchmarker()
+ new TestDoubleBenchmarker(),
+ new H2LoadBenchmarker()
];
const benchmarkers = {};
diff --git a/benchmark/http2/respond-with-fd.js b/benchmark/http2/respond-with-fd.js
new file mode 100644
index 00000000000000..d7a312c78bf4da
--- /dev/null
+++ b/benchmark/http2/respond-with-fd.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const common = require('../common.js');
+const PORT = common.PORT;
+const path = require('path');
+const fs = require('fs');
+
+const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html');
+
+var bench = common.createBenchmark(main, {
+ requests: [100, 1000, 10000, 100000, 1000000],
+ streams: [100, 200, 1000],
+ clients: [1, 2]
+}, { flags: ['--expose-http2', '--no-warnings'] });
+
+function main(conf) {
+
+ fs.open(file, 'r', (err, fd) => {
+ if (err)
+ throw err;
+
+ const n = +conf.requests;
+ const m = +conf.streams;
+ const c = +conf.clients;
+ const http2 = require('http2');
+ const server = http2.createServer();
+ server.on('stream', (stream) => {
+ stream.respondWithFD(fd);
+ stream.on('error', (err) => {});
+ });
+ server.listen(PORT, () => {
+ bench.http({
+ path: '/',
+ requests: n,
+ maxConcurrentStreams: m,
+ clients: c,
+ threads: c
+ }, () => server.close());
+ });
+
+ });
+
+}
diff --git a/benchmark/http2/simple.js b/benchmark/http2/simple.js
new file mode 100644
index 00000000000000..d12b20fc5ac773
--- /dev/null
+++ b/benchmark/http2/simple.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const common = require('../common.js');
+const PORT = common.PORT;
+
+const path = require('path');
+const fs = require('fs');
+
+const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html');
+
+var bench = common.createBenchmark(main, {
+ requests: [100, 1000, 10000, 100000],
+ streams: [100, 200, 1000],
+ clients: [1, 2]
+}, { flags: ['--expose-http2', '--no-warnings'] });
+
+function main(conf) {
+ const n = +conf.requests;
+ const m = +conf.streams;
+ const c = +conf.clients;
+ const http2 = require('http2');
+ const server = http2.createServer();
+ server.on('stream', (stream) => {
+ const out = fs.createReadStream(file);
+ stream.respond();
+ out.pipe(stream);
+ stream.on('error', (err) => {});
+ });
+ server.listen(PORT, () => {
+ bench.http({
+ path: '/',
+ requests: n,
+ maxConcurrentStreams: m,
+ clients: c,
+ threads: c
+ }, () => { server.close(); });
+ });
+}
diff --git a/test/common/index.js b/test/common/index.js
index fc14cdacacc587..a5ca4cec576e74 100644
--- a/test/common/index.js
+++ b/test/common/index.js
@@ -816,3 +816,12 @@ exports.hijackStdout = hijackStdWritable.bind(null, 'stdout');
exports.hijackStderr = hijackStdWritable.bind(null, 'stderr');
exports.restoreStdout = restoreWritable.bind(null, 'stdout');
exports.restoreStderr = restoreWritable.bind(null, 'stderr');
+
+let fd = 2;
+exports.firstInvalidFD = function firstInvalidFD() {
+ // Get first known bad file descriptor.
+ try {
+ while (fs.fstatSync(++fd));
+ } catch (e) {}
+ return fd;
+};
diff --git a/test/parallel/test-async-wrap-getasyncid.js b/test/parallel/test-async-wrap-getasyncid.js
index 57d6f86ebe5ca8..ce51408a6b678b 100644
--- a/test/parallel/test-async-wrap-getasyncid.js
+++ b/test/parallel/test-async-wrap-getasyncid.js
@@ -19,6 +19,11 @@ const providers = Object.assign({}, process.binding('async_wrap').Providers);
process.removeAllListeners('uncaughtException');
hooks.disable();
delete providers.NONE; // Should never be used.
+
+ // TODO(jasnell): Test for these
+ delete providers.HTTP2SESSION;
+ delete providers.HTTP2SESSIONSHUTDOWNWRAP;
+
const obj_keys = Object.keys(providers);
if (obj_keys.length > 0)
process._rawDebug(obj_keys);
diff --git a/test/parallel/test-dgram-bind-default-address.js b/test/parallel/test-dgram-bind-default-address.js
old mode 100755
new mode 100644
diff --git a/test/parallel/test-http2-binding.js b/test/parallel/test-http2-binding.js
new file mode 100644
index 00000000000000..c26549d3615981
--- /dev/null
+++ b/test/parallel/test-http2-binding.js
@@ -0,0 +1,229 @@
+// Flags: --expose-http2
+'use strict';
+
+require('../common');
+const assert = require('assert');
+
+assert.doesNotThrow(() => process.binding('http2'));
+
+const binding = process.binding('http2');
+const http2 = require('http2');
+
+assert(binding.Http2Session);
+assert.strictEqual(typeof binding.Http2Session, 'function');
+
+const settings = http2.getDefaultSettings();
+assert.strictEqual(settings.headerTableSize, 4096);
+assert.strictEqual(settings.enablePush, true);
+assert.strictEqual(settings.initialWindowSize, 65535);
+assert.strictEqual(settings.maxFrameSize, 16384);
+
+assert.strictEqual(binding.nghttp2ErrorString(-517),
+ 'GOAWAY has already been sent');
+
+// assert constants are present
+assert(binding.constants);
+assert.strictEqual(typeof binding.constants, 'object');
+const constants = binding.constants;
+
+const expectedStatusCodes = {
+ HTTP_STATUS_CONTINUE: 100,
+ HTTP_STATUS_SWITCHING_PROTOCOLS: 101,
+ HTTP_STATUS_PROCESSING: 102,
+ HTTP_STATUS_OK: 200,
+ HTTP_STATUS_CREATED: 201,
+ HTTP_STATUS_ACCEPTED: 202,
+ HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION: 203,
+ HTTP_STATUS_NO_CONTENT: 204,
+ HTTP_STATUS_RESET_CONTENT: 205,
+ HTTP_STATUS_PARTIAL_CONTENT: 206,
+ HTTP_STATUS_MULTI_STATUS: 207,
+ HTTP_STATUS_ALREADY_REPORTED: 208,
+ HTTP_STATUS_IM_USED: 226,
+ HTTP_STATUS_MULTIPLE_CHOICES: 300,
+ HTTP_STATUS_MOVED_PERMANENTLY: 301,
+ HTTP_STATUS_FOUND: 302,
+ HTTP_STATUS_SEE_OTHER: 303,
+ HTTP_STATUS_NOT_MODIFIED: 304,
+ HTTP_STATUS_USE_PROXY: 305,
+ HTTP_STATUS_TEMPORARY_REDIRECT: 307,
+ HTTP_STATUS_PERMANENT_REDIRECT: 308,
+ HTTP_STATUS_BAD_REQUEST: 400,
+ HTTP_STATUS_UNAUTHORIZED: 401,
+ HTTP_STATUS_PAYMENT_REQUIRED: 402,
+ HTTP_STATUS_FORBIDDEN: 403,
+ HTTP_STATUS_NOT_FOUND: 404,
+ HTTP_STATUS_METHOD_NOT_ALLOWED: 405,
+ HTTP_STATUS_NOT_ACCEPTABLE: 406,
+ HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED: 407,
+ HTTP_STATUS_REQUEST_TIMEOUT: 408,
+ HTTP_STATUS_CONFLICT: 409,
+ HTTP_STATUS_GONE: 410,
+ HTTP_STATUS_LENGTH_REQUIRED: 411,
+ HTTP_STATUS_PRECONDITION_FAILED: 412,
+ HTTP_STATUS_PAYLOAD_TOO_LARGE: 413,
+ HTTP_STATUS_URI_TOO_LONG: 414,
+ HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: 415,
+ HTTP_STATUS_RANGE_NOT_SATISFIABLE: 416,
+ HTTP_STATUS_EXPECTATION_FAILED: 417,
+ HTTP_STATUS_TEAPOT: 418,
+ HTTP_STATUS_MISDIRECTED_REQUEST: 421,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY: 422,
+ HTTP_STATUS_LOCKED: 423,
+ HTTP_STATUS_FAILED_DEPENDENCY: 424,
+ HTTP_STATUS_UNORDERED_COLLECTION: 425,
+ HTTP_STATUS_UPGRADE_REQUIRED: 426,
+ HTTP_STATUS_PRECONDITION_REQUIRED: 428,
+ HTTP_STATUS_TOO_MANY_REQUESTS: 429,
+ HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
+ HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS: 451,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR: 500,
+ HTTP_STATUS_NOT_IMPLEMENTED: 501,
+ HTTP_STATUS_BAD_GATEWAY: 502,
+ HTTP_STATUS_SERVICE_UNAVAILABLE: 503,
+ HTTP_STATUS_GATEWAY_TIMEOUT: 504,
+ HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED: 505,
+ HTTP_STATUS_VARIANT_ALSO_NEGOTIATES: 506,
+ HTTP_STATUS_INSUFFICIENT_STORAGE: 507,
+ HTTP_STATUS_LOOP_DETECTED: 508,
+ HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED: 509,
+ HTTP_STATUS_NOT_EXTENDED: 510,
+ HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED: 511
+};
+
+const expectedHeaderNames = {
+ HTTP2_HEADER_STATUS: ':status',
+ HTTP2_HEADER_METHOD: ':method',
+ HTTP2_HEADER_AUTHORITY: ':authority',
+ HTTP2_HEADER_SCHEME: ':scheme',
+ HTTP2_HEADER_PATH: ':path',
+ HTTP2_HEADER_DATE: 'date',
+ HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset',
+ HTTP2_HEADER_ACCEPT_ENCODING: 'accept-encoding',
+ HTTP2_HEADER_ACCEPT_LANGUAGE: 'accept-language',
+ HTTP2_HEADER_ACCEPT_RANGES: 'accept-ranges',
+ HTTP2_HEADER_ACCEPT: 'accept',
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: 'access-control-allow-origin',
+ HTTP2_HEADER_AGE: 'age',
+ HTTP2_HEADER_ALLOW: 'allow',
+ HTTP2_HEADER_AUTHORIZATION: 'authorization',
+ HTTP2_HEADER_CACHE_CONTROL: 'cache-control',
+ HTTP2_HEADER_CONTENT_DISPOSITION: 'content-disposition',
+ HTTP2_HEADER_CONTENT_ENCODING: 'content-encoding',
+ HTTP2_HEADER_CONTENT_LANGUAGE: 'content-language',
+ HTTP2_HEADER_CONTENT_LENGTH: 'content-length',
+ HTTP2_HEADER_CONTENT_LOCATION: 'content-location',
+ HTTP2_HEADER_CONTENT_RANGE: 'content-range',
+ HTTP2_HEADER_CONTENT_TYPE: 'content-type',
+ HTTP2_HEADER_COOKIE: 'cookie',
+ HTTP2_HEADER_CONNECTION: 'connection',
+ HTTP2_HEADER_ETAG: 'etag',
+ HTTP2_HEADER_EXPECT: 'expect',
+ HTTP2_HEADER_EXPIRES: 'expires',
+ HTTP2_HEADER_FROM: 'from',
+ HTTP2_HEADER_HOST: 'host',
+ HTTP2_HEADER_IF_MATCH: 'if-match',
+ HTTP2_HEADER_IF_MODIFIED_SINCE: 'if-modified-since',
+ HTTP2_HEADER_IF_NONE_MATCH: 'if-none-match',
+ HTTP2_HEADER_IF_RANGE: 'if-range',
+ HTTP2_HEADER_IF_UNMODIFIED_SINCE: 'if-unmodified-since',
+ HTTP2_HEADER_LAST_MODIFIED: 'last-modified',
+ HTTP2_HEADER_LINK: 'link',
+ HTTP2_HEADER_LOCATION: 'location',
+ HTTP2_HEADER_MAX_FORWARDS: 'max-forwards',
+ HTTP2_HEADER_PREFER: 'prefer',
+ HTTP2_HEADER_PROXY_AUTHENTICATE: 'proxy-authenticate',
+ HTTP2_HEADER_PROXY_AUTHORIZATION: 'proxy-authorization',
+ HTTP2_HEADER_PROXY_CONNECTION: 'proxy-connection',
+ HTTP2_HEADER_RANGE: 'range',
+ HTTP2_HEADER_REFERER: 'referer',
+ HTTP2_HEADER_REFRESH: 'refresh',
+ HTTP2_HEADER_RETRY_AFTER: 'retry-after',
+ HTTP2_HEADER_SERVER: 'server',
+ HTTP2_HEADER_SET_COOKIE: 'set-cookie',
+ HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: 'strict-transport-security',
+ HTTP2_HEADER_TRANSFER_ENCODING: 'transfer-encoding',
+ HTTP2_HEADER_USER_AGENT: 'user-agent',
+ HTTP2_HEADER_VARY: 'vary',
+ HTTP2_HEADER_VIA: 'via',
+ HTTP2_HEADER_WWW_AUTHENTICATE: 'www-authenticate',
+ HTTP2_HEADER_KEEP_ALIVE: 'keep-alive',
+ HTTP2_HEADER_CONTENT_MD5: 'content-md5',
+ HTTP2_HEADER_TE: 'te',
+ HTTP2_HEADER_UPGRADE: 'upgrade',
+ HTTP2_HEADER_HTTP2_SETTINGS: 'http2-settings'
+};
+
+const expectedNGConstants = {
+ NGHTTP2_SESSION_SERVER: 0,
+ NGHTTP2_SESSION_CLIENT: 1,
+ NGHTTP2_STREAM_STATE_IDLE: 1,
+ NGHTTP2_STREAM_STATE_OPEN: 2,
+ NGHTTP2_STREAM_STATE_RESERVED_LOCAL: 3,
+ NGHTTP2_STREAM_STATE_RESERVED_REMOTE: 4,
+ NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: 5,
+ NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: 6,
+ NGHTTP2_STREAM_STATE_CLOSED: 7,
+ NGHTTP2_HCAT_REQUEST: 0,
+ NGHTTP2_HCAT_RESPONSE: 1,
+ NGHTTP2_HCAT_PUSH_RESPONSE: 2,
+ NGHTTP2_HCAT_HEADERS: 3,
+ NGHTTP2_NO_ERROR: 0,
+ NGHTTP2_PROTOCOL_ERROR: 1,
+ NGHTTP2_INTERNAL_ERROR: 2,
+ NGHTTP2_FLOW_CONTROL_ERROR: 3,
+ NGHTTP2_SETTINGS_TIMEOUT: 4,
+ NGHTTP2_STREAM_CLOSED: 8,
+ NGHTTP2_FRAME_SIZE_ERROR: 6,
+ NGHTTP2_REFUSED_STREAM: 7,
+ NGHTTP2_CANCEL: 8,
+ NGHTTP2_COMPRESSION_ERROR: 9,
+ NGHTTP2_CONNECT_ERROR: 10,
+ NGHTTP2_ENHANCE_YOUR_CALM: 11,
+ NGHTTP2_INADEQUATE_SECURITY: 12,
+ NGHTTP2_HTTP_1_1_REQUIRED: 13,
+ NGHTTP2_NV_FLAG_NONE: 0,
+ NGHTTP2_NV_FLAG_NO_INDEX: 1,
+ NGHTTP2_ERR_DEFERRED: -508,
+ NGHTTP2_ERR_NOMEM: -901,
+ NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: -509,
+ NGHTTP2_ERR_INVALID_ARGUMENT: -501,
+ NGHTTP2_ERR_STREAM_CLOSED: -510,
+ NGHTTP2_ERR_FRAME_SIZE_ERROR: -522,
+ NGHTTP2_FLAG_NONE: 0,
+ NGHTTP2_FLAG_END_STREAM: 1,
+ NGHTTP2_FLAG_END_HEADERS: 4,
+ NGHTTP2_FLAG_ACK: 1,
+ NGHTTP2_FLAG_PADDED: 8,
+ NGHTTP2_FLAG_PRIORITY: 32,
+ NGHTTP2_DEFAULT_WEIGHT: 16,
+ NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: 1,
+ NGHTTP2_SETTINGS_ENABLE_PUSH: 2,
+ NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3,
+ NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4,
+ NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5,
+ NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6
+};
+
+const defaultSettings = {
+ DEFAULT_SETTINGS_HEADER_TABLE_SIZE: 4096,
+ DEFAULT_SETTINGS_ENABLE_PUSH: 1,
+ DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 65535,
+ DEFAULT_SETTINGS_MAX_FRAME_SIZE: 16384
+};
+
+for (const name of Object.keys(constants)) {
+ if (name.startsWith('HTTP_STATUS_')) {
+ assert.strictEqual(expectedStatusCodes[name], constants[name],
+ `Expected status code match for ${name}`);
+ } else if (name.startsWith('HTTP2_HEADER_')) {
+ assert.strictEqual(expectedHeaderNames[name], constants[name],
+ `Expected header name match for ${name}`);
+ } else if (name.startsWith('NGHTTP2_')) {
+ assert.strictEqual(expectedNGConstants[name], constants[name],
+ `Expected ng constant match for ${name}`);
+ } else if (name.startsWith('DEFAULT_SETTINGS_')) {
+ assert.strictEqual(defaultSettings[name], constants[name],
+ `Expected default setting match for ${name}`);
+ }
+}
diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js
new file mode 100644
index 00000000000000..3bd72f138ba60e
--- /dev/null
+++ b/test/parallel/test-http2-client-data-end.js
@@ -0,0 +1,90 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+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) => {
+ 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');
+ });
+ stream.end('st');
+ });
+ }
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ 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);
+
+ let expected = 2;
+ function maybeClose() {
+ if (--expected === 0) {
+ server.close();
+ client.destroy();
+ }
+ }
+
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[':status'], 200);
+ assert.strictEqual(headers['content-type'], 'text/html');
+ }));
+
+ client.on('stream', common.mustCall((stream, headers, flags) => {
+ assert.strictEqual(headers[':scheme'], 'http');
+ assert.strictEqual(headers[':path'], '/foobar');
+ assert.strictEqual(headers[':authority'], `localhost:${port}`);
+ stream.on('push', common.mustCall((headers, flags) => {
+ assert.strictEqual(headers[':status'], 200);
+ assert.strictEqual(headers['content-type'], 'text/html');
+ assert.strictEqual(headers['x-push-data'], 'pushed by server');
+ }));
+
+ stream.setEncoding('utf8');
+ let pushData = '';
+ stream.on('data', common.mustCall((d) => {
+ pushData += d;
+ }, 2));
+ stream.on('end', common.mustCall(() => {
+ assert.strictEqual(pushData, 'pushed by server data');
+ maybeClose();
+ }));
+ }));
+
+ let data = '';
+
+ req.setEncoding('utf8');
+ req.on('data', common.mustCall((d) => data += d));
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(data, 'test');
+ maybeClose();
+ }));
+ req.end();
+}));
diff --git a/test/parallel/test-http2-client-destroy-before-connect.js b/test/parallel/test-http2-client-destroy-before-connect.js
new file mode 100644
index 00000000000000..1f6b087dd220b3
--- /dev/null
+++ b/test/parallel/test-http2-client-destroy-before-connect.js
@@ -0,0 +1,28 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+// we use the lower-level API here
+server.on('stream', common.mustNotCall());
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+ client.destroy();
+
+ req.on('response', common.mustNotCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-client-destroy-before-request.js b/test/parallel/test-http2-client-destroy-before-request.js
new file mode 100644
index 00000000000000..71519d5903b58f
--- /dev/null
+++ b/test/parallel/test-http2-client-destroy-before-request.js
@@ -0,0 +1,28 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+// we use the lower-level API here
+server.on('stream', common.mustNotCall());
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+ client.destroy();
+
+ assert.throws(() => client.request({ ':path': '/' }),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_SESSION',
+ message: /^The session has been destroyed$/
+ }));
+
+ server.close();
+
+}));
diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js
new file mode 100644
index 00000000000000..56cfec5d65a223
--- /dev/null
+++ b/test/parallel/test-http2-client-destroy.js
@@ -0,0 +1,54 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+server.listen(0);
+
+server.on('listening', common.mustCall(function() {
+ const port = this.address().port;
+
+ const destroyCallbacks = [
+ (client) => client.destroy(),
+ (client) => client.socket.destroy()
+ ];
+
+ let remaining = destroyCallbacks.length;
+
+ destroyCallbacks.forEach((destroyCallback) => {
+ const client = h2.connect(`http://localhost:${port}`);
+ client.on('connect', common.mustCall(() => {
+ const socket = client.socket;
+
+ assert(client.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.socket, '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();
+ }
+ }));
+ });
+}));
diff --git a/test/parallel/test-http2-client-priority-before-connect.js b/test/parallel/test-http2-client-priority-before-connect.js
new file mode 100644
index 00000000000000..68933b2d83bbf1
--- /dev/null
+++ b/test/parallel/test-http2-client-priority-before-connect.js
@@ -0,0 +1,37 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+ client.priority(req, {});
+
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js
new file mode 100644
index 00000000000000..33b6cb354fe225
--- /dev/null
+++ b/test/parallel/test-http2-client-rststream-before-connect.js
@@ -0,0 +1,34 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+ client.rstStream(req, 0);
+ assert.strictEqual(req.rstCode, 0);
+
+ // make sure that destroy is called
+ req._destroy = common.mustCall(req._destroy.bind(req));
+
+ req.on('streamClosed', common.mustCall((code) => {
+ assert.strictEqual(req.destroyed, true);
+ assert.strictEqual(code, 0);
+ server.close();
+ client.destroy();
+ }));
+
+ req.on('response', common.mustNotCall());
+ 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
new file mode 100644
index 00000000000000..314a88a63c2d16
--- /dev/null
+++ b/test/parallel/test-http2-client-set-priority.js
@@ -0,0 +1,49 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+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.end('test');
+ }));
+
+ server.listen(0, common.mustCall(() => {
+ const port = server.address().port;
+ const client = http2.connect(`http://localhost:${port}`);
+
+ const headers = { ':path': '/' };
+ const req = client.request(headers, { weight: actual });
+
+ req.on('data', common.mustCall(() => {}));
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+ }));
+};
+
+// when client weight is lower than 1, weight is 1
+checkWeight(-1, 1);
+checkWeight(0, 1);
+
+// 1 - 256 is correct weight
+checkWeight(1, 1);
+checkWeight(16, 16);
+checkWeight(256, 256);
+
+// when client weight is higher than 256, weight is 256
+checkWeight(257, 256);
+checkWeight(512, 256);
+
+// when client weight is undefined, weight is default 16
+checkWeight(undefined, 16);
diff --git a/test/parallel/test-http2-client-settings-before-connect.js b/test/parallel/test-http2-client-settings-before-connect.js
new file mode 100644
index 00000000000000..9391502479a7f9
--- /dev/null
+++ b/test/parallel/test-http2-client-settings-before-connect.js
@@ -0,0 +1,63 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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(() => {
+
+ 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);
+ });
+
+ client.settings({ maxFrameSize: 1234567 });
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-client-shutdown-before-connect.js b/test/parallel/test-http2-client-shutdown-before-connect.js
new file mode 100644
index 00000000000000..203963bf57721e
--- /dev/null
+++ b/test/parallel/test-http2-client-shutdown-before-connect.js
@@ -0,0 +1,23 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+// we use the lower-level API here
+server.on('stream', common.mustNotCall());
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ client.shutdown({graceful: true}, common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+
+}));
diff --git a/test/parallel/test-http2-client-socket-destroy.js b/test/parallel/test-http2-client-socket-destroy.js
new file mode 100644
index 00000000000000..fe2d92753172a8
--- /dev/null
+++ b/test/parallel/test-http2-client-socket-destroy.js
@@ -0,0 +1,46 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const h2 = require('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) {
+ // The stream aborted event must have been triggered
+ stream.on('aborted', common.mustCall());
+
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.write(body);
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(function() {
+ const client = h2.connect(`http://localhost:${this.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustCall(() => {
+ // send a premature socket close
+ client.socket.destroy();
+ }));
+ req.on('data', common.mustNotCall());
+
+ req.on('end', 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
new file mode 100644
index 00000000000000..5ab0cac5082aed
--- /dev/null
+++ b/test/parallel/test-http2-client-stream-destroy-before-connect.js
@@ -0,0 +1,63 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+const NGHTTP2_INTERNAL_ERROR = h2.constants.NGHTTP2_INTERNAL_ERROR;
+
+const server = h2.createServer();
+
+// Do not mustCall the server side callbacks, they may or may not be called
+// depending on the OS. The determination is based largely on operating
+// system specific timings
+server.on('stream', (stream) => {
+ // Do not wrap in a must call or use common.expectsError (which now uses
+ // must call). The error may or may not be reported depending on operating
+ // system specific timings.
+ stream.on('error', (err) => {
+ if (err) {
+ assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR');
+ assert.strictEqual(err.message, 'Stream closed with error code 2');
+ }
+ });
+ stream.respond({});
+ stream.end();
+});
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+ const err = new Error('test');
+ req.destroy(err);
+
+ req.on('error', common.mustCall((err) => {
+ const fn = err.code === 'ERR_HTTP2_STREAM_ERROR' ?
+ common.expectsError({
+ code: 'ERR_HTTP2_STREAM_ERROR',
+ type: Error,
+ message: 'Stream closed with error code 2'
+ }) :
+ common.expectsError({
+ type: Error,
+ message: 'test'
+ });
+ fn(err);
+ }, 2));
+
+ req.on('streamClosed', common.mustCall((code) => {
+ assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR);
+ assert.strictEqual(code, NGHTTP2_INTERNAL_ERROR);
+ server.close();
+ client.destroy();
+ }));
+
+ 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
new file mode 100644
index 00000000000000..d92d40492e204c
--- /dev/null
+++ b/test/parallel/test-http2-client-unescaped-path.js
@@ -0,0 +1,37 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+server.on('stream', common.mustNotCall());
+
+const count = 32;
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ let remaining = count;
+ function maybeClose() {
+ if (--remaining === 0) {
+ server.close();
+ client.destroy();
+ }
+ }
+
+ // nghttp2 will catch the bad header value for us.
+ function doTest(i) {
+ const req = client.request({ ':path': `bad${String.fromCharCode(i)}path` });
+ req.on('error', common.expectsError({
+ code: 'ERR_HTTP2_STREAM_ERROR',
+ type: Error,
+ message: 'Stream closed with error code 1'
+ }));
+ req.on('streamClosed', common.mustCall(maybeClose));
+ }
+
+ for (let i = 0; i <= count; i += 1)
+ doTest(i);
+}));
diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js
new file mode 100644
index 00000000000000..4ce7da878e1fd2
--- /dev/null
+++ b/test/parallel/test-http2-client-upload.js
@@ -0,0 +1,44 @@
+// Flags: --expose-http2
+'use strict';
+
+// Verifies that uploading data from a client works
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+const fs = require('fs');
+const path = require('path');
+
+const loc = path.join(common.fixturesDir, 'person.jpg');
+let fileData;
+
+assert(fs.existsSync(loc));
+
+fs.readFile(loc, common.mustCall((err, data) => {
+ assert.ifError(err);
+ fileData = data;
+
+ const server = http2.createServer();
+
+ server.on('stream', common.mustCall((stream) => {
+ let data = Buffer.alloc(0);
+ stream.on('data', (chunk) => data = Buffer.concat([data, chunk]));
+ stream.on('end', common.mustCall(() => {
+ assert.deepStrictEqual(data, fileData);
+ }));
+ stream.respond();
+ stream.end();
+ }));
+
+ server.listen(0, common.mustCall(() => {
+ 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(() => {
+ server.close();
+ client.destroy();
+ }));
+ fs.createReadStream(loc).pipe(req);
+ }));
+}));
diff --git a/test/parallel/test-http2-client-write-before-connect.js b/test/parallel/test-http2-client-write-before-connect.js
new file mode 100644
index 00000000000000..f58fc5c43f69b6
--- /dev/null
+++ b/test/parallel/test-http2-client-write-before-connect.js
@@ -0,0 +1,53 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+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) {
+ 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(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({
+ [HTTP2_HEADER_PATH]: '/',
+ [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST });
+ req.write('some data ');
+ req.write('more data');
+
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-compat-serverrequest-headers.js b/test/parallel/test-http2-compat-serverrequest-headers.js
new file mode 100644
index 00000000000000..32af86314b1675
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverrequest-headers.js
@@ -0,0 +1,70 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// Http2ServerRequest should have header helpers
+
+const server = h2.createServer();
+server.listen(0, common.mustCall(function() {
+ const port = server.address().port;
+ server.once('request', common.mustCall(function(request, response) {
+ const expected = {
+ ':path': '/foobar',
+ ':method': 'GET',
+ ':scheme': 'http',
+ ':authority': `localhost:${port}`,
+ 'foo-bar': 'abc123'
+ };
+
+ assert.strictEqual(request.method, expected[':method']);
+ assert.strictEqual(request.scheme, expected[':scheme']);
+ assert.strictEqual(request.path, expected[':path']);
+ assert.strictEqual(request.url, expected[':path']);
+ assert.strictEqual(request.authority, expected[':authority']);
+
+ const headers = request.headers;
+ for (const [name, value] of Object.entries(expected)) {
+ assert.strictEqual(headers[name], value);
+ }
+
+ const rawHeaders = request.rawHeaders;
+ for (const [name, value] of Object.entries(expected)) {
+ const position = rawHeaders.indexOf(name);
+ assert.notStrictEqual(position, -1);
+ assert.strictEqual(rawHeaders[position + 1], value);
+ }
+
+ request.url = '/one';
+ assert.strictEqual(request.url, '/one');
+ assert.strictEqual(request.path, '/one');
+
+ request.path = '/two';
+ assert.strictEqual(request.url, '/two');
+ assert.strictEqual(request.path, '/two');
+
+ response.on('finish', common.mustCall(function() {
+ server.close();
+ }));
+ response.end();
+ }));
+
+ const url = `http://localhost:${port}`;
+ const client = h2.connect(url, common.mustCall(function() {
+ const headers = {
+ ':path': '/foobar',
+ ':method': 'GET',
+ ':scheme': 'http',
+ ':authority': `localhost:${port}`,
+ 'foo-bar': 'abc123'
+ };
+ const request = client.request(headers);
+ request.on('end', common.mustCall(function() {
+ client.destroy();
+ }));
+ request.end();
+ request.resume();
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverrequest.js b/test/parallel/test-http2-compat-serverrequest.js
new file mode 100644
index 00000000000000..d54f554848ce09
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverrequest.js
@@ -0,0 +1,52 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+const net = require('net');
+
+// Http2ServerRequest should expose convenience properties
+
+const server = h2.createServer();
+server.listen(0, common.mustCall(function() {
+ const port = server.address().port;
+ server.once('request', common.mustCall(function(request, response) {
+ const expected = {
+ statusCode: null,
+ version: '2.0',
+ httpVersionMajor: 2,
+ httpVersionMinor: 0
+ };
+
+ assert.strictEqual(request.statusCode, expected.statusCode);
+ assert.strictEqual(request.httpVersion, expected.version);
+ assert.strictEqual(request.httpVersionMajor, expected.httpVersionMajor);
+ assert.strictEqual(request.httpVersionMinor, expected.httpVersionMinor);
+
+ assert.ok(request.socket instanceof net.Socket);
+ assert.ok(request.connection instanceof net.Socket);
+ assert.strictEqual(request.socket, request.connection);
+
+ response.on('finish', common.mustCall(function() {
+ server.close();
+ }));
+ response.end();
+ }));
+
+ 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('end', common.mustCall(function() {
+ client.destroy();
+ }));
+ request.end();
+ request.resume();
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js
new file mode 100644
index 00000000000000..68e438d62ff96d
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js
@@ -0,0 +1,79 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// Push a request & response
+
+const pushExpect = 'This is a server-initiated response';
+const servExpect = 'This is a client-initiated response';
+
+const server = h2.createServer((request, response) => {
+ assert.strictEqual(response.stream.id % 2, 1);
+ response.write(servExpect);
+
+ response.createPushResponse({
+ ':path': '/pushed',
+ ':method': 'GET'
+ }, common.mustCall((error, push) => {
+ assert.ifError(error);
+ assert.strictEqual(push.stream.id % 2, 0);
+ push.end(pushExpect);
+ response.end();
+ }));
+});
+
+server.listen(0, common.mustCall(() => {
+ const port = server.address().port;
+
+ const client = h2.connect(`http://localhost:${port}`, common.mustCall(() => {
+ const headers = {
+ ':path': '/',
+ ':method': 'GET',
+ };
+
+ let remaining = 2;
+ function maybeClose() {
+ if (--remaining === 0) {
+ client.destroy();
+ server.close();
+ }
+ }
+
+ const req = client.request(headers);
+
+ client.on('stream', common.mustCall((pushStream, headers) => {
+ assert.strictEqual(headers[':path'], '/pushed');
+ assert.strictEqual(headers[':method'], 'GET');
+ assert.strictEqual(headers[':scheme'], 'http');
+ assert.strictEqual(headers[':authority'], `localhost:${port}`);
+
+ let actual = '';
+ pushStream.on('push', common.mustCall((headers) => {
+ assert.strictEqual(headers[':status'], 200);
+ assert(headers['date']);
+ }));
+ pushStream.setEncoding('utf8');
+ pushStream.on('data', (chunk) => actual += chunk);
+ pushStream.on('end', common.mustCall(() => {
+ assert.strictEqual(actual, pushExpect);
+ maybeClose();
+ }));
+ }));
+
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[':status'], 200);
+ assert(headers['date']);
+ }));
+
+ let actual = '';
+ req.setEncoding('utf8');
+ req.on('data', (chunk) => actual += chunk);
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(actual, servExpect);
+ maybeClose();
+ }));
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverresponse-end.js b/test/parallel/test-http2-compat-serverresponse-end.js
new file mode 100644
index 00000000000000..1274f3d6b3c148
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-end.js
@@ -0,0 +1,77 @@
+// Flags: --expose-http2
+'use strict';
+
+const { strictEqual } = require('assert');
+const { mustCall, mustNotCall } = require('../common');
+const {
+ createServer,
+ connect,
+ constants: {
+ HTTP2_HEADER_STATUS,
+ HTTP_STATUS_OK
+ }
+} = require('http2');
+
+{
+ // Http2ServerResponse.end callback is called only the first time,
+ // but may be invoked repeatedly without throwing errors.
+ const server = createServer(mustCall((request, response) => {
+ response.end(mustCall(() => {
+ server.close();
+ }));
+ 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': 'GET',
+ ':scheme': 'http',
+ ':authority': `localhost:${port}`
+ };
+ const request = client.request(headers);
+ request.on('data', mustNotCall());
+ request.on('end', mustCall(() => client.destroy()));
+ request.end();
+ request.resume();
+ }));
+ }));
+}
+
+{
+ // Http2ServerResponse.end is not necessary on HEAD requests since the stream
+ // is already closed. Headers, however, can still be sent to the client.
+ const server = createServer(mustCall((request, response) => {
+ strictEqual(response.finished, true);
+ response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
+ response.flushHeaders();
+ 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
+ strictEqual(headers.foo, 'bar');
+ }));
+ request.on('data', mustNotCall());
+ request.on('end', mustCall(() => {
+ client.destroy();
+ server.close();
+ }));
+ request.end();
+ request.resume();
+ }));
+ }));
+}
diff --git a/test/parallel/test-http2-compat-serverresponse-finished.js b/test/parallel/test-http2-compat-serverresponse-finished.js
new file mode 100644
index 00000000000000..e5739e5ac3e2f6
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-finished.js
@@ -0,0 +1,37 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// 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) {
+ response.on('finish', common.mustCall(function() {
+ server.close();
+ }));
+ assert.strictEqual(response.finished, false);
+ response.end();
+ assert.strictEqual(response.finished, true);
+ }));
+
+ 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();
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverresponse-flushheaders.js b/test/parallel/test-http2-compat-serverresponse-flushheaders.js
new file mode 100644
index 00000000000000..4bfe4909121c19
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-flushheaders.js
@@ -0,0 +1,43 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// Http2ServerResponse.flushHeaders
+
+const server = h2.createServer();
+server.listen(0, common.mustCall(function() {
+ const port = server.address().port;
+ server.once('request', common.mustCall(function(request, response) {
+ response.flushHeaders();
+ response.flushHeaders(); // Idempotent
+ response.writeHead(400, {'foo-bar': 'abc123'}); // Ignored
+
+ response.on('finish', common.mustCall(function() {
+ server.close();
+ }));
+ response.end();
+ }));
+
+ 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('response', common.mustCall(function(headers, flags) {
+ assert.strictEqual(headers['foo-bar'], undefined);
+ assert.strictEqual(headers[':status'], 200);
+ }, 1));
+ request.on('end', common.mustCall(function() {
+ client.destroy();
+ }));
+ request.end();
+ request.resume();
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverresponse-headers.js b/test/parallel/test-http2-compat-serverresponse-headers.js
new file mode 100644
index 00000000000000..28bd36ce2e46c5
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-headers.js
@@ -0,0 +1,83 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// Http2ServerResponse should support checking and reading custom headers
+
+const server = h2.createServer();
+server.listen(0, common.mustCall(function() {
+ const port = server.address().port;
+ server.once('request', common.mustCall(function(request, response) {
+ const real = 'foo-bar';
+ const fake = 'bar-foo';
+ const denormalised = ` ${real.toUpperCase()}\n\t`;
+ const expectedValue = 'abc123';
+
+ response.setHeader(real, expectedValue);
+
+ assert.strictEqual(response.hasHeader(real), true);
+ assert.strictEqual(response.hasHeader(fake), false);
+ assert.strictEqual(response.hasHeader(denormalised), true);
+ assert.strictEqual(response.getHeader(real), expectedValue);
+ assert.strictEqual(response.getHeader(denormalised), expectedValue);
+ assert.strictEqual(response.getHeader(fake), undefined);
+
+ response.removeHeader(fake);
+ assert.strictEqual(response.hasHeader(fake), false);
+
+ response.setHeader(real, expectedValue);
+ assert.strictEqual(response.getHeader(real), expectedValue);
+ assert.strictEqual(response.hasHeader(real), true);
+ response.removeHeader(real);
+ assert.strictEqual(response.hasHeader(real), false);
+
+ response.setHeader(denormalised, expectedValue);
+ assert.strictEqual(response.getHeader(denormalised), expectedValue);
+ assert.strictEqual(response.hasHeader(denormalised), true);
+ response.removeHeader(denormalised);
+ assert.strictEqual(response.hasHeader(denormalised), false);
+
+ assert.throws(function() {
+ response.setHeader(':status', 'foobar');
+ }, Error);
+ assert.throws(function() {
+ response.setHeader(real, null);
+ }, TypeError);
+ assert.throws(function() {
+ response.setHeader(real, undefined);
+ }, TypeError);
+
+ response.setHeader(real, expectedValue);
+ const expectedHeaderNames = [real];
+ assert.deepStrictEqual(response.getHeaderNames(), expectedHeaderNames);
+ const expectedHeaders = {[real]: expectedValue};
+ assert.deepStrictEqual(response.getHeaders(), expectedHeaders);
+
+ response.getHeaders()[fake] = fake;
+ assert.strictEqual(response.hasHeader(fake), false);
+
+ response.on('finish', common.mustCall(function() {
+ server.close();
+ }));
+ response.end();
+ }));
+
+ 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();
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverresponse-statuscode.js b/test/parallel/test-http2-compat-serverresponse-statuscode.js
new file mode 100644
index 00000000000000..201a63c379bc8b
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-statuscode.js
@@ -0,0 +1,76 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// Http2ServerResponse should have a statusCode property
+
+const server = h2.createServer();
+server.listen(0, common.mustCall(function() {
+ const port = server.address().port;
+ server.once('request', common.mustCall(function(request, response) {
+ const expectedDefaultStatusCode = 200;
+ const realStatusCodes = {
+ continue: 100,
+ ok: 200,
+ multipleChoices: 300,
+ badRequest: 400,
+ internalServerError: 500
+ };
+ const fakeStatusCodes = {
+ tooLow: 99,
+ tooHigh: 600
+ };
+
+ assert.strictEqual(response.statusCode, expectedDefaultStatusCode);
+
+ assert.doesNotThrow(function() {
+ response.statusCode = realStatusCodes.ok;
+ response.statusCode = realStatusCodes.multipleChoices;
+ response.statusCode = realStatusCodes.badRequest;
+ response.statusCode = realStatusCodes.internalServerError;
+ });
+
+ assert.throws(function() {
+ response.statusCode = realStatusCodes.continue;
+ }, common.expectsError({
+ code: 'ERR_HTTP2_INFO_STATUS_NOT_ALLOWED',
+ type: RangeError
+ }));
+ assert.throws(function() {
+ response.statusCode = fakeStatusCodes.tooLow;
+ }, common.expectsError({
+ code: 'ERR_HTTP2_STATUS_INVALID',
+ type: RangeError
+ }));
+ assert.throws(function() {
+ response.statusCode = fakeStatusCodes.tooHigh;
+ }, common.expectsError({
+ code: 'ERR_HTTP2_STATUS_INVALID',
+ type: RangeError
+ }));
+
+ response.on('finish', common.mustCall(function() {
+ server.close();
+ }));
+ response.end();
+ }));
+
+ 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();
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage.js b/test/parallel/test-http2-compat-serverresponse-statusmessage.js
new file mode 100644
index 00000000000000..08822c99390835
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-statusmessage.js
@@ -0,0 +1,52 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// Http2ServerResponse.writeHead should accept an optional status message
+
+const unsupportedWarned = common.mustCall(1);
+process.on('warning', ({name, message}) => {
+ const expectedMessage =
+ 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)';
+ if (name === 'UnsupportedWarning' && message === expectedMessage)
+ unsupportedWarned();
+});
+
+const server = h2.createServer();
+server.listen(0, common.mustCall(function() {
+ const port = server.address().port;
+ server.once('request', common.mustCall(function(request, response) {
+ const statusCode = 200;
+ const statusMessage = 'OK';
+ const headers = {'foo-bar': 'abc123'};
+ response.writeHead(statusCode, statusMessage, headers);
+
+ response.on('finish', common.mustCall(function() {
+ server.close();
+ }));
+ response.end();
+ }));
+
+ 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('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers[':status'], 200);
+ assert.strictEqual(headers['foo-bar'], 'abc123');
+ }, 1));
+ request.on('end', common.mustCall(function() {
+ client.destroy();
+ }));
+ request.end();
+ request.resume();
+ }));
+}));
diff --git a/test/parallel/test-http2-compat-serverresponse-write-no-cb.js b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js
new file mode 100644
index 00000000000000..2428cebd8bcf09
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js
@@ -0,0 +1,98 @@
+// Flags: --expose-http2
+'use strict';
+
+const { throws } = require('assert');
+const { mustCall, mustNotCall, expectsError } = require('../common');
+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();
+ request.resume();
+ }));
+
+ server.once('request', mustCall((request, response) => {
+ client.destroy();
+ response.stream.session.on('close', mustCall(() => {
+ response.on('error', mustNotCall());
+ throws(
+ () => { response.write('muahaha'); },
+ /The stream is already closed/
+ );
+ server.close();
+ }));
+ }));
+ }));
+}
+
+{
+ 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();
+ request.resume();
+ }));
+
+ server.once('request', mustCall((request, response) => {
+ client.destroy();
+ response.stream.session.on('close', mustCall(() => {
+ response.write('muahaha', mustCall(expectedError));
+ server.close();
+ }));
+ }));
+ }));
+}
+
+{
+ 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();
+ request.resume();
+ }));
+
+ server.once('request', mustCall((request, response) => {
+ response.stream.session.on('close', mustCall(() => {
+ response.write('muahaha', 'utf8', mustCall(expectedError));
+ server.close();
+ }));
+ client.destroy();
+ }));
+ }));
+}
diff --git a/test/parallel/test-http2-compat-serverresponse-writehead.js b/test/parallel/test-http2-compat-serverresponse-writehead.js
new file mode 100644
index 00000000000000..b4c531d3393282
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-writehead.js
@@ -0,0 +1,44 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+// Http2ServerResponse.writeHead should override previous headers
+
+const server = h2.createServer();
+server.listen(0, common.mustCall(function() {
+ const port = server.address().port;
+ server.once('request', common.mustCall(function(request, response) {
+ response.setHeader('foo-bar', 'def456');
+ response.writeHead(500);
+ response.writeHead(418, {'foo-bar': 'abc123'}); // Override
+
+ response.on('finish', common.mustCall(function() {
+ assert.doesNotThrow(() => { response.writeHead(300); });
+ server.close();
+ }));
+ response.end();
+ }));
+
+ 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('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers['foo-bar'], 'abc123');
+ assert.strictEqual(headers[':status'], 418);
+ }, 1));
+ request.on('end', common.mustCall(function() {
+ client.destroy();
+ }));
+ request.end();
+ request.resume();
+ }));
+}));
diff --git a/test/parallel/test-http2-connect-method.js b/test/parallel/test-http2-connect-method.js
new file mode 100644
index 00000000000000..05ff96a3cd1320
--- /dev/null
+++ b/test/parallel/test-http2-connect-method.js
@@ -0,0 +1,71 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const net = require('net');
+const http2 = require('http2');
+const { URL } = require('url');
+
+const {
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_AUTHORITY,
+ NGHTTP2_CONNECT_ERROR
+} = http2.constants;
+
+const server = net.createServer(common.mustCall((socket) => {
+ let data = '';
+ socket.setEncoding('utf8');
+ socket.on('data', (chunk) => data += chunk);
+ socket.on('end', common.mustCall(() => {
+ assert.strictEqual(data, 'hello');
+ }));
+ socket.on('close', common.mustCall());
+ socket.end('hello');
+}));
+
+server.listen(0, common.mustCall(() => {
+
+ const port = server.address().port;
+
+ const proxy = http2.createServer();
+ proxy.on('stream', common.mustCall((stream, headers) => {
+ if (headers[HTTP2_HEADER_METHOD] !== 'CONNECT') {
+ stream.rstWithRefused();
+ return;
+ }
+ const auth = new URL(`tcp://${headers[HTTP2_HEADER_AUTHORITY]}`);
+ assert.strictEqual(auth.hostname, 'localhost');
+ assert.strictEqual(+auth.port, port);
+ const socket = net.connect(auth.port, auth.hostname, () => {
+ stream.respond();
+ socket.pipe(stream);
+ stream.pipe(socket);
+ });
+ socket.on('close', common.mustCall());
+ socket.on('error', (error) => {
+ stream.rstStream(NGHTTP2_CONNECT_ERROR);
+ });
+ }));
+
+ proxy.listen(0, () => {
+ const client = http2.connect(`http://localhost:${proxy.address().port}`);
+
+ const req = client.request({
+ [HTTP2_HEADER_METHOD]: 'CONNECT',
+ [HTTP2_HEADER_AUTHORITY]: `localhost:${port}`,
+ });
+
+ req.on('response', common.mustCall());
+ let data = '';
+ req.setEncoding('utf8');
+ req.on('data', (chunk) => data += chunk);
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(data, 'hello');
+ client.destroy();
+ proxy.close();
+ server.close();
+ }));
+ req.end('hello');
+ });
+}));
diff --git a/test/parallel/test-http2-connect.js b/test/parallel/test-http2-connect.js
new file mode 100644
index 00000000000000..305ea034c902e4
--- /dev/null
+++ b/test/parallel/test-http2-connect.js
@@ -0,0 +1,29 @@
+// Flags: --expose-http2
+'use strict';
+
+const { mustCall } = require('../common');
+const { doesNotThrow } = require('assert');
+const { createServer, connect } = require('http2');
+
+const server = createServer();
+server.listen(0, mustCall(() => {
+ const authority = `http://localhost:${server.address().port}`;
+ const options = {};
+ const listener = () => mustCall();
+
+ const clients = new Set();
+ doesNotThrow(() => clients.add(connect(authority)));
+ doesNotThrow(() => clients.add(connect(authority, options)));
+ doesNotThrow(() => clients.add(connect(authority, options, listener())));
+ doesNotThrow(() => clients.add(connect(authority, listener())));
+
+ for (const client of clients) {
+ client.once('connect', mustCall((headers) => {
+ client.destroy();
+ clients.delete(client);
+ if (clients.size === 0) {
+ server.close();
+ }
+ }));
+ }
+}));
diff --git a/test/parallel/test-http2-cookies.js b/test/parallel/test-http2-cookies.js
new file mode 100644
index 00000000000000..297b3966df9f18
--- /dev/null
+++ b/test/parallel/test-http2-cookies.js
@@ -0,0 +1,62 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+const setCookie = [
+ 'a=b',
+ 'c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly'
+];
+
+// we use the lower-level API here
+server.on('stream', common.mustCall(onStream));
+
+function onStream(stream, headers, flags) {
+
+ assert(Array.isArray(headers.abc));
+ assert.strictEqual(headers.abc.length, 3);
+ assert.strictEqual(headers.abc[0], '1');
+ assert.strictEqual(headers.abc[1], '2');
+ assert.strictEqual(headers.abc[2], '3');
+ assert.strictEqual(typeof headers.cookie, 'string');
+ assert.strictEqual(headers.cookie, 'a=b; c=d; e=f');
+
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200,
+ 'set-cookie': setCookie
+ });
+
+ stream.end('hello world');
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({
+ ':path': '/',
+ abc: [1, 2, 3],
+ cookie: ['a=b', 'c=d', 'e=f'],
+ });
+ req.resume();
+
+ req.on('response', common.mustCall((headers) => {
+ assert(Array.isArray(headers['set-cookie']));
+ assert.deepStrictEqual(headers['set-cookie'], setCookie,
+ 'set-cookie header does not match');
+ }));
+
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-create-client-connect.js b/test/parallel/test-http2-create-client-connect.js
new file mode 100644
index 00000000000000..8173dc3d08658f
--- /dev/null
+++ b/test/parallel/test-http2-create-client-connect.js
@@ -0,0 +1,88 @@
+// Flags: --expose-http2
+'use strict';
+
+// Tests http2.connect()
+
+const common = require('../common');
+const fs = require('fs');
+const h2 = require('http2');
+const path = require('path');
+const url = require('url');
+const URL = url.URL;
+
+{
+ const server = h2.createServer();
+ server.listen(0);
+
+ server.on('listening', common.mustCall(function() {
+ const port = this.address().port;
+
+ const items = [
+ [`http://localhost:${port}`],
+ [new URL(`http://localhost:${port}`)],
+ [url.parse(`http://localhost:${port}`)],
+ [{port: port}, {protocol: 'http:'}],
+ [{port: port, hostname: '127.0.0.1'}, {protocol: 'http:'}]
+ ];
+
+ let count = items.length;
+
+ const maybeClose = common.mustCall((client) => {
+ client.destroy();
+ if (--count === 0) {
+ setImmediate(() => server.close());
+ }
+ }, items.length);
+
+ items.forEach((i) => {
+ const client =
+ h2.connect.apply(null, i)
+ .on('connect', common.mustCall(() => maybeClose(client)));
+ });
+
+ // Will fail because protocol does not match the server.
+ h2.connect({port: port, protocol: 'https:'})
+ .on('socketError', common.mustCall());
+ }));
+}
+
+
+{
+
+ const options = {
+ key: fs.readFileSync(path.join(common.fixturesDir, 'keys/agent3-key.pem')),
+ cert: fs.readFileSync(path.join(common.fixturesDir, 'keys/agent3-cert.pem'))
+ };
+
+ const server = h2.createSecureServer(options);
+ server.listen(0);
+
+ server.on('listening', common.mustCall(function() {
+ const port = this.address().port;
+
+ const opts = {rejectUnauthorized: false};
+
+ const items = [
+ [`https://localhost:${port}`, opts],
+ [new URL(`https://localhost:${port}`), opts],
+ [url.parse(`https://localhost:${port}`), opts],
+ [{port: port, protocol: 'https:'}, opts],
+ [{port: port, hostname: '127.0.0.1', protocol: 'https:'}, opts]
+ ];
+
+ let count = items.length;
+
+ const maybeClose = common.mustCall((client) => {
+ client.destroy();
+ if (--count === 0) {
+ setImmediate(() => server.close());
+ }
+ }, items.length);
+
+ items.forEach((i) => {
+ const client =
+ h2.connect.apply(null, i)
+ .on('connect', common.mustCall(() => maybeClose(client)));
+ });
+ }));
+}
diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js
new file mode 100644
index 00000000000000..9b1cf4a0c9ee86
--- /dev/null
+++ b/test/parallel/test-http2-create-client-secure-session.js
@@ -0,0 +1,75 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+const tls = require('tls');
+const h2 = require('http2');
+
+function loadKey(keyname) {
+ return fs.readFileSync(
+ path.join(common.fixturesDir, 'keys', keyname), 'binary');
+}
+
+function onStream(stream, headers) {
+ const socket = stream.session.socket;
+ assert(headers[':authority'].startsWith(socket.servername));
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end(JSON.stringify({
+ servername: socket.servername,
+ alpnProtocol: socket.alpnProtocol
+ }));
+}
+
+function verifySecureSession(key, cert, ca, opts) {
+ const server = h2.createSecureServer({cert, key});
+ server.on('stream', common.mustCall(onStream));
+ server.listen(0);
+ server.on('listening', common.mustCall(function() {
+ const headers = { ':path': '/' };
+ if (!opts) {
+ opts = {};
+ }
+ opts.secureContext = tls.createSecureContext({ca});
+ const client = h2.connect(`https://localhost:${this.address().port}`, opts, function() {
+ const req = client.request(headers);
+
+ req.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers[':status'], 200, 'status code is set');
+ assert.strictEqual(headers['content-type'], 'text/html',
+ 'content type is set');
+ assert(headers['date'], 'there is a date');
+ }));
+
+ let data = '';
+ req.setEncoding('utf8');
+ req.on('data', (d) => data += d);
+ req.on('end', common.mustCall(() => {
+ const jsonData = JSON.parse(data);
+ assert.strictEqual(jsonData.servername, opts.servername || 'localhost');
+ assert.strictEqual(jsonData.alpnProtocol, 'h2');
+ server.close();
+ client.socket.destroy();
+ }));
+ req.end();
+ });
+ }));
+}
+
+// The server can be connected as 'localhost'.
+verifySecureSession(
+ loadKey('agent8-key.pem'),
+ loadKey('agent8-cert.pem'),
+ loadKey('fake-startcom-root-cert.pem'));
+
+// Custom servername is specified.
+verifySecureSession(
+ loadKey('agent1-key.pem'),
+ loadKey('agent1-cert.pem'),
+ loadKey('ca1-cert.pem'),
+ {servername: 'agent1'});
diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js
new file mode 100644
index 00000000000000..c1c6ce1bfea62e
--- /dev/null
+++ b/test/parallel/test-http2-create-client-session.js
@@ -0,0 +1,61 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+const body =
+ 'this is some data';
+
+const server = h2.createServer();
+const count = 100;
+
+// we use the lower-level API here
+server.on('stream', common.mustCall(onStream, count));
+
+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.end(body);
+}
+
+server.listen(0);
+
+let expected = count;
+
+server.on('listening', common.mustCall(function() {
+
+ const client = h2.connect(`http://localhost:${this.address().port}`);
+
+ const headers = { ':path': '/' };
+
+ for (let n = 0; n < count; n++) {
+ const req = client.request(headers);
+
+ req.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers[':status'], 200, 'status code is set');
+ assert.strictEqual(headers['content-type'], 'text/html',
+ 'content type is set');
+ assert(headers['date'], 'there is a date');
+ }));
+
+ let data = '';
+ req.setEncoding('utf8');
+ req.on('data', (d) => data += d);
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(body, data);
+ if (--expected === 0) {
+ server.close();
+ client.destroy();
+ }
+ }));
+ req.end();
+ }
+
+}));
diff --git a/test/parallel/test-http2-date-header.js b/test/parallel/test-http2-date-header.js
new file mode 100644
index 00000000000000..d9a73b2ef61d4b
--- /dev/null
+++ b/test/parallel/test-http2-date-header.js
@@ -0,0 +1,28 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+server.on('stream', common.mustCall((stream) => {
+ // Date header is defaulted
+ stream.respond();
+ stream.end();
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+ req.on('response', common.mustCall((headers) => {
+ // The date header must be set to a non-invalid value
+ assert.notStrictEqual((new Date()).toString(), 'Invalid Date');
+ }));
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+}));
diff --git a/test/parallel/test-http2-dont-override.js b/test/parallel/test-http2-dont-override.js
new file mode 100644
index 00000000000000..55b29580fbc9f4
--- /dev/null
+++ b/test/parallel/test-http2-dont-override.js
@@ -0,0 +1,48 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const options = {};
+
+const server = http2.createServer(options);
+
+// options are defaulted but the options are not modified
+assert.deepStrictEqual(Object.keys(options), []);
+
+server.on('stream', common.mustCall((stream) => {
+ const headers = {};
+ const options = {};
+ stream.respond(headers, options);
+
+ // The headers are defaulted but the original object is not modified
+ assert.deepStrictEqual(Object.keys(headers), []);
+
+ // Options are defaulted but the original object is not modified
+ assert.deepStrictEqual(Object.keys(options), []);
+
+ stream.end();
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ const headers = {};
+ const options = {};
+
+ const req = client.request(headers, options);
+
+ // The headers are defaulted but the original object is not modified
+ assert.deepStrictEqual(Object.keys(headers), []);
+
+ // Options are defaulted but the original object is not modified
+ assert.deepStrictEqual(Object.keys(options), []);
+
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+}));
diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js
new file mode 100644
index 00000000000000..0c1a1bcceea255
--- /dev/null
+++ b/test/parallel/test-http2-getpackedsettings.js
@@ -0,0 +1,131 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x05,
+ 0x00, 0x00, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00,
+ 0xff, 0xff, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);
+const val = http2.getPackedSettings(http2.getDefaultSettings());
+assert.deepStrictEqual(val, check);
+
+[
+ ['headerTableSize', 0],
+ ['headerTableSize', 2 ** 32 - 1],
+ ['initialWindowSize', 0],
+ ['initialWindowSize', 2 ** 32 - 1],
+ ['maxFrameSize', 16384],
+ ['maxFrameSize', 2 ** 24 - 1],
+ ['maxConcurrentStreams', 0],
+ ['maxConcurrentStreams', 2 ** 31 - 1],
+ ['maxHeaderListSize', 0],
+ ['maxHeaderListSize', 2 ** 32 - 1]
+].forEach((i) => {
+ assert.doesNotThrow(() => http2.getPackedSettings({ [i[0]]: i[1] }));
+});
+
+assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: true }));
+assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false }));
+
+[
+ ['headerTableSize', -1],
+ ['headerTableSize', 2 ** 32],
+ ['initialWindowSize', -1],
+ ['initialWindowSize', 2 ** 32],
+ ['maxFrameSize', 16383],
+ ['maxFrameSize', 2 ** 24],
+ ['maxConcurrentStreams', -1],
+ ['maxConcurrentStreams', 2 ** 31],
+ ['maxHeaderListSize', -1],
+ ['maxHeaderListSize', 2 ** 32]
+].forEach((i) => {
+ assert.throws(() => {
+ http2.getPackedSettings({ [i[0]]: i[1] });
+ }, common.expectsError({
+ code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+ type: RangeError,
+ message: `Invalid value for setting "${i[0]}": ${i[1]}`
+ }));
+});
+
+[
+ 1, null, '', Infinity, new Date(), {}, NaN, [false]
+].forEach((i) => {
+ assert.throws(() => {
+ http2.getPackedSettings({ enablePush: i });
+ }, common.expectsError({
+ code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+ type: TypeError,
+ message: `Invalid value for setting "enablePush": ${i}`
+ }));
+});
+
+{
+ const check = Buffer.from([
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00,
+ 0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04,
+ 0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64,
+ 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);
+
+ const packed = http2.getPackedSettings({
+ headerTableSize: 100,
+ initialWindowSize: 100,
+ maxFrameSize: 20000,
+ maxConcurrentStreams: 200,
+ maxHeaderListSize: 100,
+ enablePush: true,
+ foo: 'ignored'
+ });
+ assert.strictEqual(packed.length, 36);
+ assert.deepStrictEqual(packed, check);
+}
+
+{
+ const packed = Buffer.from([
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00,
+ 0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04,
+ 0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64,
+ 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);
+
+ [1, true, '', [], {}, NaN].forEach((i) => {
+ assert.throws(() => {
+ http2.getUnpackedSettings(i);
+ }, common.expectsError({
+ code: 'ERR_INVALID_ARG_TYPE',
+ type: TypeError,
+ message: 'The "buf" argument must be one of type Buffer or Uint8Array'
+ }));
+ });
+
+ assert.throws(() => {
+ http2.getUnpackedSettings(packed.slice(5));
+ }, common.expectsError({
+ code: 'ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
+ type: RangeError,
+ message: 'Packed settings length must be a multiple of six'
+ }));
+
+ const settings = http2.getUnpackedSettings(packed);
+
+ assert(settings);
+ assert.strictEqual(settings.headerTableSize, 100);
+ assert.strictEqual(settings.initialWindowSize, 100);
+ assert.strictEqual(settings.maxFrameSize, 20000);
+ assert.strictEqual(settings.maxConcurrentStreams, 200);
+ assert.strictEqual(settings.maxHeaderListSize, 100);
+ assert.strictEqual(settings.enablePush, true);
+}
+
+{
+ const packed = Buffer.from([0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF]);
+
+ assert.throws(() => {
+ http2.getUnpackedSettings(packed, {validate: true});
+ }, common.expectsError({
+ code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+ type: RangeError,
+ message: 'Invalid value for setting "maxConcurrentStreams": 4294967295'
+ }));
+}
diff --git a/test/parallel/test-http2-goaway-opaquedata.js b/test/parallel/test-http2-goaway-opaquedata.js
new file mode 100644
index 00000000000000..e5904adf3bee99
--- /dev/null
+++ b/test/parallel/test-http2-goaway-opaquedata.js
@@ -0,0 +1,38 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+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.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);
+ assert.deepStrictEqual(data, buf);
+ server.close();
+ }));
+ const req = client.request({ ':path': '/' });
+ req.resume();
+ req.on('end', common.mustCall());
+ req.end();
+
+});
diff --git a/test/parallel/test-http2-head-request.js b/test/parallel/test-http2-head-request.js
new file mode 100644
index 00000000000000..07f0eb6c93298f
--- /dev/null
+++ b/test/parallel/test-http2-head-request.js
@@ -0,0 +1,57 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const errCheck = common.expectsError({
+ type: Error,
+ message: 'write after end'
+}, 2);
+
+const {
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_STATUS,
+ HTTP2_METHOD_HEAD,
+} = http2.constants;
+
+const server = http2.createServer();
+server.on('stream', (stream, headers) => {
+
+ assert.strictEqual(headers[HTTP2_HEADER_METHOD], HTTP2_METHOD_HEAD);
+
+ stream.respond({ [HTTP2_HEADER_STATUS]: 200 });
+
+ // Because this is a head request, the outbound stream is closed automatically
+ stream.on('error', common.mustCall(errCheck));
+ stream.write('data');
+});
+
+
+server.listen(0, () => {
+
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({
+ [HTTP2_HEADER_METHOD]: HTTP2_METHOD_HEAD,
+ [HTTP2_HEADER_PATH]: '/'
+ });
+
+ // Because it is a HEAD request, the payload is meaningless. The
+ // option.endStream flag is set automatically making the stream
+ // non-writable.
+ req.on('error', common.mustCall(errCheck));
+ req.write('data');
+
+ req.on('response', common.mustCall((headers, flags) => {
+ assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200);
+ assert.strictEqual(flags, 5); // the end of stream flag is set
+ }));
+ req.on('data', common.mustNotCall());
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+});
diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js
new file mode 100644
index 00000000000000..b0424397f22696
--- /dev/null
+++ b/test/parallel/test-http2-https-fallback.js
@@ -0,0 +1,146 @@
+// Flags: --expose-http2
+'use strict';
+
+const {
+ fixturesDir,
+ mustCall,
+ mustNotCall
+} = require('../common');
+const { strictEqual } = require('assert');
+const { join } = require('path');
+const { readFileSync } = require('fs');
+const { createSecureContext } = require('tls');
+const { createSecureServer, connect } = require('http2');
+const { get } = require('https');
+const { parse } = require('url');
+const { connect: tls } = require('tls');
+
+const countdown = (count, done) => () => --count === 0 && done();
+
+function loadKey(keyname) {
+ return readFileSync(join(fixturesDir, 'keys', keyname));
+}
+
+const key = loadKey('agent8-key.pem');
+const cert = loadKey('agent8-cert.pem');
+const ca = loadKey('fake-startcom-root-cert.pem');
+
+const clientOptions = { secureContext: createSecureContext({ ca }) };
+
+function onRequest(request, response) {
+ const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ?
+ request.stream.session : request;
+ response.writeHead(200, { 'content-type': 'application/json' });
+ response.end(JSON.stringify({
+ alpnProtocol,
+ httpVersion: request.httpVersion
+ }));
+}
+
+function onSession(session) {
+ const headers = {
+ ':path': '/',
+ ':method': 'GET',
+ ':scheme': 'https',
+ ':authority': `localhost:${this.server.address().port}`
+ };
+
+ const request = session.request(headers);
+ request.on('response', mustCall((headers) => {
+ strictEqual(headers[':status'], 200);
+ strictEqual(headers['content-type'], 'application/json');
+ }));
+ request.setEncoding('utf8');
+ let raw = '';
+ request.on('data', (chunk) => { raw += chunk; });
+ request.on('end', mustCall(() => {
+ const { alpnProtocol, httpVersion } = JSON.parse(raw);
+ strictEqual(alpnProtocol, 'h2');
+ strictEqual(httpVersion, '2.0');
+
+ session.destroy();
+ this.cleanup();
+ }));
+ request.end();
+}
+
+// HTTP/2 & HTTP/1.1 server
+{
+ const server = createSecureServer(
+ { cert, key, allowHTTP1: true },
+ mustCall(onRequest, 2)
+ );
+
+ server.listen(0);
+
+ server.on('listening', mustCall(() => {
+ const { port } = server.address();
+ const origin = `https://localhost:${port}`;
+
+ const cleanup = countdown(2, () => server.close());
+
+ // HTTP/2 client
+ connect(
+ origin,
+ clientOptions,
+ mustCall(onSession.bind({ cleanup, server }))
+ );
+
+ // HTTP/1.1 client
+ get(
+ Object.assign(parse(origin), clientOptions),
+ mustCall((response) => {
+ strictEqual(response.statusCode, 200);
+ strictEqual(response.statusMessage, 'OK');
+ strictEqual(response.headers['content-type'], 'application/json');
+
+ response.setEncoding('utf8');
+ let raw = '';
+ response.on('data', (chunk) => { raw += chunk; });
+ response.on('end', mustCall(() => {
+ const { alpnProtocol, httpVersion } = JSON.parse(raw);
+ strictEqual(alpnProtocol, false);
+ strictEqual(httpVersion, '1.1');
+
+ cleanup();
+ }));
+ })
+ );
+ }));
+}
+
+// HTTP/2-only server
+{
+ const server = createSecureServer(
+ { cert, key },
+ mustCall(onRequest)
+ );
+
+ server.on('unknownProtocol', mustCall((socket) => {
+ socket.destroy();
+ }, 2));
+
+ server.listen(0);
+
+ server.on('listening', mustCall(() => {
+ const { port } = server.address();
+ const origin = `https://localhost:${port}`;
+
+ const cleanup = countdown(3, () => server.close());
+
+ // HTTP/2 client
+ connect(
+ origin,
+ clientOptions,
+ mustCall(onSession.bind({ cleanup, server }))
+ );
+
+ // HTTP/1.1 client
+ get(Object.assign(parse(origin), clientOptions), mustNotCall())
+ .on('error', mustCall(cleanup));
+
+ // Incompatible ALPN TLS client
+ tls(Object.assign({ port, ALPNProtocols: ['fake'] }, clientOptions))
+ .on('error', mustCall(cleanup));
+ }));
+}
diff --git a/test/parallel/test-http2-info-headers.js b/test/parallel/test-http2-info-headers.js
new file mode 100755
index 00000000000000..c5d93d514f5d28
--- /dev/null
+++ b/test/parallel/test-http2-info-headers.js
@@ -0,0 +1,85 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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));
+
+const status101regex =
+ /^HTTP status code 101 \(Switching Protocols\) is forbidden in HTTP\/2$/;
+const afterRespondregex =
+ /^Cannot specify additional headers after response initiated$/;
+
+function onStream(stream, headers, flags) {
+
+ assert.throws(() => stream.additionalHeaders({ ':status': 201 }),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_INFO_STATUS',
+ type: RangeError,
+ message: /^Invalid informational status code: 201$/
+ }));
+
+ assert.throws(() => stream.additionalHeaders({ ':status': 101 }),
+ common.expectsError({
+ code: 'ERR_HTTP2_STATUS_101',
+ type: Error,
+ message: status101regex
+ }));
+
+ // Can send more than one
+ stream.additionalHeaders({ ':status': 100 });
+ stream.additionalHeaders({ ':status': 100 });
+
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+
+ assert.throws(() => stream.additionalHeaders({ abc: 123 }),
+ common.expectsError({
+ code: 'ERR_HTTP2_HEADERS_AFTER_RESPOND',
+ type: Error,
+ message: afterRespondregex
+ }));
+
+ stream.end('hello world');
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/'});
+
+ // The additionalHeaders method does not exist on client stream
+ assert.strictEqual(req.additionalHeaders, undefined);
+
+ // Additional informational headers
+ req.on('headers', common.mustCall((headers) => {
+ assert.notStrictEqual(headers, undefined);
+ assert.strictEqual(headers[':status'], 100);
+ }, 2));
+
+ // Response headers
+ req.on('response', common.mustCall((headers) => {
+ assert.notStrictEqual(headers, undefined);
+ assert.strictEqual(headers[':status'], 200);
+ assert.strictEqual(headers['content-type'], 'text/html');
+ }));
+
+ req.resume();
+
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-max-concurrent-streams.js b/test/parallel/test-http2-max-concurrent-streams.js
new file mode 100644
index 00000000000000..6725a7c7545a90
--- /dev/null
+++ b/test/parallel/test-http2-max-concurrent-streams.js
@@ -0,0 +1,67 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const {
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_PATH,
+ HTTP2_METHOD_POST
+} = h2.constants;
+
+// 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.end('hello world');
+}));
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ let reqs = 2;
+ function onEnd() {
+ if (--reqs === 0) {
+ server.close();
+ client.destroy();
+ }
+ }
+
+ 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'
+ })));
+
+}));
diff --git a/test/parallel/test-http2-methods.js b/test/parallel/test-http2-methods.js
new file mode 100644
index 00000000000000..1a8828f22c7363
--- /dev/null
+++ b/test/parallel/test-http2-methods.js
@@ -0,0 +1,48 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+const methods = [undefined, 'GET', 'POST', 'PATCH', 'FOO', 'A B C'];
+let expected = methods.length;
+
+// we use the lower-level API here
+server.on('stream', common.mustCall(onStream, expected));
+
+function onStream(stream, headers, flags) {
+ const method = headers[':method'];
+ assert.notStrictEqual(method, undefined);
+ assert(methods.includes(method), `method ${method} not included`);
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end('hello world');
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const headers = { ':path': '/' };
+
+ methods.forEach((method) => {
+ headers[':method'] = method;
+ const req = client.request(headers);
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ if (--expected === 0) {
+ server.close();
+ client.destroy();
+ }
+ }));
+ req.end();
+ });
+}));
diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js
new file mode 100644
index 00000000000000..e356169d26e7e6
--- /dev/null
+++ b/test/parallel/test-http2-misused-pseudoheaders.js
@@ -0,0 +1,61 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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) {
+
+ [
+ ':path',
+ ':authority',
+ ':method',
+ ':scheme'
+ ].forEach((i) => {
+ assert.throws(() => stream.respond({[i]: '/'}),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_PSEUDOHEADER'
+ }));
+ });
+
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+
+ // This will cause an error to be emitted on the stream because
+ // using a pseudo-header in a trailer is forbidden.
+ stream.on('fetchTrailers', (trailers) => {
+ trailers[':status'] = 'bar';
+ });
+
+ stream.on('error', common.expectsError({
+ code: 'ERR_HTTP2_INVALID_PSEUDOHEADER'
+ }));
+
+ stream.end('hello world');
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-multi-content-length.js b/test/parallel/test-http2-multi-content-length.js
new file mode 100644
index 00000000000000..5dcd56990be5dd
--- /dev/null
+++ b/test/parallel/test-http2-multi-content-length.js
@@ -0,0 +1,58 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+server.on('stream', common.mustCall((stream) => {
+ stream.respond();
+ stream.end();
+}));
+
+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();
+ }
+ }
+
+ // 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({
+ 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
+ const req2 = client.request({
+ ':method': 'POST',
+ 'content-length': 1
+ });
+ req2.resume();
+ req2.on('end', common.mustCall(maybeClose));
+ req2.end('a');
+
+ // Request 3 will fail because nghttp2 does not allow the content-length
+ // header to be set for non-payload bearing requests...
+ const req3 = client.request({ 'content-length': 1});
+ req3.resume();
+ req3.on('end', common.mustCall(maybeClose));
+ req3.on('error', common.expectsError({
+ code: 'ERR_HTTP2_STREAM_ERROR',
+ type: Error,
+ message: 'Stream closed with error code 1'
+ }));
+}));
diff --git a/test/parallel/test-http2-multiheaders.js b/test/parallel/test-http2-multiheaders.js
new file mode 100644
index 00000000000000..d7b8f56d51ac30
--- /dev/null
+++ b/test/parallel/test-http2-multiheaders.js
@@ -0,0 +1,60 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+const src = Object.create(null);
+src.accept = [ 'abc', 'def' ];
+src.Accept = 'ghijklmnop';
+src['www-authenticate'] = 'foo';
+src['WWW-Authenticate'] = 'bar';
+src['WWW-AUTHENTICATE'] = 'baz';
+src['proxy-authenticate'] = 'foo';
+src['Proxy-Authenticate'] = 'bar';
+src['PROXY-AUTHENTICATE'] = 'baz';
+src['x-foo'] = 'foo';
+src['X-Foo'] = 'bar';
+src['X-FOO'] = 'baz';
+src.constructor = 'foo';
+src.Constructor = 'bar';
+src.CONSTRUCTOR = 'baz';
+// eslint-disable-next-line no-proto
+src['__proto__'] = 'foo';
+src['__PROTO__'] = 'bar';
+src['__Proto__'] = 'baz';
+
+function checkHeaders(headers) {
+ assert.deepStrictEqual(headers['accept'],
+ [ 'abc', 'def', 'ghijklmnop' ]);
+ assert.deepStrictEqual(headers['www-authenticate'],
+ [ 'foo', 'bar', 'baz' ]);
+ assert.deepStrictEqual(headers['proxy-authenticate'],
+ [ 'foo', 'bar', 'baz' ]);
+ assert.deepStrictEqual(headers['x-foo'], [ 'foo', 'bar', 'baz' ]);
+ assert.deepStrictEqual(headers['constructor'], [ 'foo', 'bar', 'baz' ]);
+ // eslint-disable-next-line no-proto
+ assert.deepStrictEqual(headers['__proto__'], [ 'foo', 'bar', 'baz' ]);
+}
+
+server.on('stream', common.mustCall((stream, headers) => {
+ assert.strictEqual(headers[':path'], '/');
+ assert.strictEqual(headers[':scheme'], 'http');
+ assert.strictEqual(headers[':method'], 'GET');
+ checkHeaders(headers);
+ stream.respond(src);
+ stream.end();
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request(src);
+ req.on('response', common.mustCall(checkHeaders));
+ req.on('streamClosed', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+}));
diff --git a/test/parallel/test-http2-multiplex.js b/test/parallel/test-http2-multiplex.js
new file mode 100644
index 00000000000000..b6b81c73a654c6
--- /dev/null
+++ b/test/parallel/test-http2-multiplex.js
@@ -0,0 +1,59 @@
+// Flags: --expose-http2
+'use strict';
+
+// Tests opening 100 concurrent simultaneous uploading streams over a single
+// connection and makes sure that the data for each is appropriately echoed.
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+const count = 100;
+
+server.on('stream', common.mustCall((stream) => {
+ stream.respond();
+ stream.pipe(stream);
+}, count));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ let remaining = count;
+
+ function maybeClose() {
+ if (--remaining === 0) {
+ server.close();
+ client.destroy();
+ }
+ }
+
+ function doRequest() {
+ const req = client.request({ ':method': 'POST '});
+
+ let data = '';
+ req.setEncoding('utf8');
+ req.on('data', (chunk) => data += chunk);
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(data, 'abcdefghij');
+ maybeClose();
+ }));
+
+ let n = 0;
+ function writeChunk() {
+ if (n < 10) {
+ req.write(String.fromCharCode(97 + n));
+ setTimeout(writeChunk, 10);
+ } else {
+ req.end();
+ }
+ n++;
+ }
+
+ writeChunk();
+ }
+
+ for (let n = 0; n < count; n++)
+ doRequest();
+}));
diff --git a/test/parallel/test-http2-noflag.js b/test/parallel/test-http2-noflag.js
new file mode 100644
index 00000000000000..a1e0e8b72c79e9
--- /dev/null
+++ b/test/parallel/test-http2-noflag.js
@@ -0,0 +1,8 @@
+// The --expose-http2 flag is not set
+'use strict';
+
+require('../common');
+const assert = require('assert');
+
+assert.throws(() => require('http2'),
+ /^Error: Cannot find module 'http2'$/);
diff --git a/test/parallel/test-http2-options-max-headers-block-length.js b/test/parallel/test-http2-options-max-headers-block-length.js
new file mode 100644
index 00000000000000..41e8d549b4a9a3
--- /dev/null
+++ b/test/parallel/test-http2-options-max-headers-block-length.js
@@ -0,0 +1,48 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+// we use the lower-level API here
+server.on('stream', common.mustNotCall());
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ // Setting the maxSendHeaderBlockLength, then attempting to send a
+ // headers block that is too big should cause a 'meError' to
+ // be emitted, and will cause the stream to be shutdown.
+ const options = {
+ maxSendHeaderBlockLength: 10
+ };
+
+ const client = h2.connect(`http://localhost:${server.address().port}`,
+ options);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustNotCall());
+
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+
+ req.on('frameError', common.mustCall((type, code) => {
+ assert.strictEqual(code, h2.constants.NGHTTP2_ERR_FRAME_SIZE_ERROR);
+ }));
+
+ req.on('error', common.mustCall(common.expectsError({
+ code: 'ERR_HTTP2_STREAM_ERROR',
+ type: Error,
+ message: 'Stream closed with error code 7'
+ })));
+
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js
new file mode 100644
index 00000000000000..1173b58e287de2
--- /dev/null
+++ b/test/parallel/test-http2-options-max-reserved-streams.js
@@ -0,0 +1,73 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+// 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 });
+ pushedStream.end();
+ pushedStream.on('aborted', common.mustNotCall());
+ }));
+
+ // The second pushStream will be aborted because the client
+ // 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 });
+ 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'
+ })));
+ }));
+
+ stream.end('hello world');
+}));
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const options = {
+ maxReservedRemoteStreams: 1
+ };
+
+ const client = h2.connect(`http://localhost:${server.address().port}`,
+ options);
+
+ const req = client.request({ ':path': '/' });
+
+ // 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('end', common.mustCall());
+ }));
+
+ req.on('response', common.mustCall());
+
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-padding-callback.js b/test/parallel/test-http2-padding-callback.js
new file mode 100644
index 00000000000000..610b636fdcc263
--- /dev/null
+++ b/test/parallel/test-http2-padding-callback.js
@@ -0,0 +1,50 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+const { PADDING_STRATEGY_CALLBACK } = h2.constants;
+
+function selectPadding(frameLen, max) {
+ assert.strictEqual(typeof frameLen, 'number');
+ assert.strictEqual(typeof max, 'number');
+ assert(max >= frameLen);
+ return max;
+}
+
+// selectPadding will be called three times:
+// 1. For the client request headers frame
+// 2. For the server response headers frame
+// 3. For the server response data frame
+const options = {
+ paddingStrategy: PADDING_STRATEGY_CALLBACK,
+ selectPadding: common.mustCall(selectPadding, 3)
+};
+
+const server = h2.createServer(options);
+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(() => {
+ const client = h2.connect(`http://localhost:${server.address().port}`,
+ options);
+
+ const req = client.request({ ':path': '/' });
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+}));
diff --git a/test/parallel/test-http2-priority-event.js b/test/parallel/test-http2-priority-event.js
new file mode 100644
index 00000000000000..bbb248265e8402
--- /dev/null
+++ b/test/parallel/test-http2-priority-event.js
@@ -0,0 +1,60 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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 onPriority(stream, parent, weight, exclusive) {
+ assert.strictEqual(stream, 1);
+ assert.strictEqual(parent, 0);
+ assert.strictEqual(weight, 1);
+ assert.strictEqual(exclusive, false);
+}
+
+function onStream(stream, headers, flags) {
+ stream.priority({
+ parent: 0,
+ weight: 1,
+ exclusive: false
+ });
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end('hello world');
+}
+
+server.listen(0);
+
+server.on('priority', common.mustCall(onPriority));
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request({ ':path': '/'});
+
+ client.on('connect', () => {
+ req.priority({
+ parent: 0,
+ weight: 1,
+ exclusive: false
+ });
+ });
+
+ req.on('priority', common.mustCall(onPriority));
+
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-respond-file-204.js b/test/parallel/test-http2-respond-file-204.js
new file mode 100644
index 00000000000000..66840e57adac91
--- /dev/null
+++ b/test/parallel/test-http2-respond-file-204.js
@@ -0,0 +1,41 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+const assert = require('assert');
+const path = require('path');
+
+const {
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_STATUS
+} = http2.constants;
+
+const fname = path.resolve(common.fixturesDir, 'elipses.txt');
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ assert.throws(() => {
+ stream.respondWithFile(fname, {
+ [HTTP2_HEADER_STATUS]: 204,
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+ });
+ }, common.expectsError({
+ code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN',
+ type: Error,
+ message: 'Responses with 204 status must not have a payload'
+ }));
+ stream.respond({});
+ stream.end();
+});
+server.listen(0, () => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+ req.on('response', common.mustCall());
+ req.on('data', common.mustNotCall());
+ req.on('end', common.mustCall(() => {
+ client.destroy();
+ server.close();
+ }));
+ req.end();
+});
diff --git a/test/parallel/test-http2-respond-file-304.js b/test/parallel/test-http2-respond-file-304.js
new file mode 100644
index 00000000000000..0b279223f14d5d
--- /dev/null
+++ b/test/parallel/test-http2-respond-file-304.js
@@ -0,0 +1,44 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+const assert = require('assert');
+const path = require('path');
+
+const {
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_STATUS
+} = http2.constants;
+
+const fname = path.resolve(common.fixturesDir, 'elipses.txt');
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respondWithFile(fname, {
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+ }, {
+ statCheck(stat, headers) {
+ // abort the send and return a 304 Not Modified instead
+ stream.respond({ [HTTP2_HEADER_STATUS]: 304 });
+ return false;
+ }
+ });
+});
+server.listen(0, () => {
+
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[HTTP2_HEADER_STATUS], 304);
+ assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE, undefined]);
+ }));
+
+ req.on('data', common.mustNotCall());
+ req.on('end', common.mustCall(() => {
+ client.destroy();
+ server.close();
+ }));
+ req.end();
+});
diff --git a/test/parallel/test-http2-respond-file-compat.js b/test/parallel/test-http2-respond-file-compat.js
new file mode 100644
index 00000000000000..be256ee2bfa416
--- /dev/null
+++ b/test/parallel/test-http2-respond-file-compat.js
@@ -0,0 +1,23 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+const path = require('path');
+
+const fname = path.resolve(common.fixturesDir, 'elipses.txt');
+
+const server = http2.createServer(common.mustCall((request, response) => {
+ response.stream.respondWithFile(fname);
+}));
+server.listen(0, () => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+ req.on('response', common.mustCall());
+ req.on('end', common.mustCall(() => {
+ client.destroy();
+ server.close();
+ }));
+ req.end();
+ req.resume();
+});
diff --git a/test/parallel/test-http2-respond-file-fd-invalid.js b/test/parallel/test-http2-respond-file-fd-invalid.js
new file mode 100644
index 00000000000000..f46dbd9dc1d1a4
--- /dev/null
+++ b/test/parallel/test-http2-respond-file-fd-invalid.js
@@ -0,0 +1,37 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+const assert = require('assert');
+
+const {
+ NGHTTP2_INTERNAL_ERROR
+} = http2.constants;
+
+const errorCheck = common.expectsError({
+ code: 'ERR_HTTP2_STREAM_ERROR',
+ type: Error,
+ message: `Stream closed with error code ${NGHTTP2_INTERNAL_ERROR}`
+}, 2);
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respondWithFD(common.firstInvalidFD());
+ stream.on('error', common.mustCall(errorCheck));
+});
+server.listen(0, () => {
+
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+
+ req.on('response', common.mustCall());
+ req.on('error', common.mustCall(errorCheck));
+ req.on('data', common.mustNotCall());
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR);
+ client.destroy();
+ server.close();
+ }));
+ req.end();
+});
diff --git a/test/parallel/test-http2-respond-file-fd.js b/test/parallel/test-http2-respond-file-fd.js
new file mode 100644
index 00000000000000..4e982bca3cfd35
--- /dev/null
+++ b/test/parallel/test-http2-respond-file-fd.js
@@ -0,0 +1,46 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const {
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_CONTENT_LENGTH
+} = http2.constants;
+
+const fname = path.resolve(common.fixturesDir, 'elipses.txt');
+const data = fs.readFileSync(fname);
+const stat = fs.statSync(fname);
+const fd = fs.openSync(fname, 'r');
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respondWithFD(fd, {
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
+ [HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
+ });
+});
+server.on('close', common.mustCall(() => fs.closeSync(fd)));
+server.listen(0, () => {
+
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
+ assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length);
+ }));
+ req.setEncoding('utf8');
+ let check = '';
+ req.on('data', (chunk) => check += chunk);
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(check, data.toString('utf8'));
+ client.destroy();
+ server.close();
+ }));
+ req.end();
+});
diff --git a/test/parallel/test-http2-respond-file-push.js b/test/parallel/test-http2-respond-file-push.js
new file mode 100644
index 00000000000000..1c2476f173463a
--- /dev/null
+++ b/test/parallel/test-http2-respond-file-push.js
@@ -0,0 +1,81 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const {
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_LAST_MODIFIED
+} = http2.constants;
+
+const fname = path.resolve(common.fixturesDir, 'elipses.txt');
+const data = fs.readFileSync(fname);
+const stat = fs.statSync(fname);
+const fd = fs.openSync(fname, 'r');
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respond({});
+ stream.end();
+
+ stream.pushStream({
+ ':path': '/file.txt',
+ ':method': 'GET'
+ }, (stream) => {
+ stream.respondWithFD(fd, {
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
+ [HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
+ [HTTP2_HEADER_LAST_MODIFIED]: stat.mtime.toUTCString()
+ });
+ });
+
+ stream.end();
+});
+
+server.on('close', common.mustCall(() => fs.closeSync(fd)));
+
+server.listen(0, () => {
+
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ let expected = 2;
+ function maybeClose() {
+ if (--expected === 0) {
+ server.close();
+ client.destroy();
+ }
+ }
+
+ const req = client.request({});
+
+ req.on('response', common.mustCall());
+
+ client.on('stream', common.mustCall((stream, headers) => {
+
+ stream.on('push', common.mustCall((headers) => {
+ assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
+ assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length);
+ assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED],
+ stat.mtime.toUTCString());
+ }));
+
+ stream.setEncoding('utf8');
+ let check = '';
+ stream.on('data', (chunk) => check += chunk);
+ stream.on('end', common.mustCall(() => {
+ assert.strictEqual(check, data.toString('utf8'));
+ maybeClose();
+ }));
+
+ }));
+
+ req.resume();
+ req.on('end', maybeClose);
+
+ req.end();
+});
diff --git a/test/parallel/test-http2-respond-file.js b/test/parallel/test-http2-respond-file.js
new file mode 100644
index 00000000000000..81babb58fadcd1
--- /dev/null
+++ b/test/parallel/test-http2-respond-file.js
@@ -0,0 +1,51 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+
+const {
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_LAST_MODIFIED
+} = http2.constants;
+
+const fname = path.resolve(common.fixturesDir, 'elipses.txt');
+const data = fs.readFileSync(fname);
+const stat = fs.statSync(fname);
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respondWithFile(fname, {
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+ }, {
+ statCheck(stat, headers) {
+ headers[HTTP2_HEADER_LAST_MODIFIED] = stat.mtime.toUTCString();
+ headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size;
+ }
+ });
+});
+server.listen(0, () => {
+
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
+ assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length);
+ assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED],
+ stat.mtime.toUTCString());
+ }));
+ req.setEncoding('utf8');
+ let check = '';
+ req.on('data', (chunk) => check += chunk);
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(check, data.toString('utf8'));
+ client.destroy();
+ server.close();
+ }));
+ req.end();
+});
diff --git a/test/parallel/test-http2-response-splitting.js b/test/parallel/test-http2-response-splitting.js
new file mode 100644
index 00000000000000..088c675389f5ba
--- /dev/null
+++ b/test/parallel/test-http2-response-splitting.js
@@ -0,0 +1,75 @@
+// Flags: --expose-http2
+'use strict';
+
+// Response splitting is no longer an issue with HTTP/2. The underlying
+// nghttp2 implementation automatically strips out the header values that
+// contain invalid characters.
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+const { URL } = require('url');
+
+// Response splitting example, credit: Amit Klein, Safebreach
+const str = '/welcome?lang=bar%c4%8d%c4%8aContentLength:%200%c4%8d%c4%8a%c' +
+ '4%8d%c4%8aHTTP/1.1%20200%20OK%c4%8d%c4%8aContentLength:%202' +
+ '0%c4%8d%c4%8aLastModified:%20Mon,%2027%20Oct%202003%2014:50:18' +
+ '%20GMT%c4%8d%c4%8aContentType:%20text/html%c4%8d%c4%8a%c4%8' +
+ 'd%c4%8a%3chtml%3eGotcha!%3c/html%3e';
+
+// Response splitting example, credit: Сковорода Никита Андреевич (@ChALkeR)
+const x = 'fooഊSet-Cookie: foo=barഊഊ';
+const y = 'foo⠊Set-Cookie: foo=bar';
+
+let remaining = 3;
+
+function makeUrl(headers) {
+ return `${headers[':scheme']}://${headers[':authority']}${headers[':path']}`;
+}
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream, headers) => {
+
+ const obj = Object.create(null);
+ switch (remaining--) {
+ case 3:
+ const url = new URL(makeUrl(headers));
+ obj[':status'] = 302;
+ obj.Location = `/foo?lang=${url.searchParams.get('lang')}`;
+ break;
+ case 2:
+ obj.foo = x;
+ break;
+ case 1:
+ obj.foo = y;
+ break;
+ }
+ stream.respond(obj);
+ stream.end();
+}, 3));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ function maybeClose() {
+ if (remaining === 0) {
+ server.close();
+ client.destroy();
+ }
+ }
+
+ function doTest(path, key, expected) {
+ const req = client.request({ ':path': path });
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers.foo, undefined);
+ assert.strictEqual(headers.location, undefined);
+ }));
+ req.resume();
+ req.on('end', common.mustCall(maybeClose));
+ }
+
+ doTest(str, 'location', str);
+ doTest('/', 'foo', x);
+ doTest('/', 'foo', y);
+
+}));
diff --git a/test/parallel/test-http2-serve-file.js b/test/parallel/test-http2-serve-file.js
new file mode 100644
index 00000000000000..109270327489eb
--- /dev/null
+++ b/test/parallel/test-http2-serve-file.js
@@ -0,0 +1,82 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+const fs = require('fs');
+const path = require('path');
+const tls = require('tls');
+
+const ajs_data = fs.readFileSync(path.resolve(common.fixturesDir, 'a.js'),
+ 'utf8');
+
+const {
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_STATUS
+} = http2.constants;
+
+function loadKey(keyname) {
+ return fs.readFileSync(
+ path.join(common.fixturesDir, 'keys', keyname), 'binary');
+}
+
+const key = loadKey('agent8-key.pem');
+const cert = loadKey('agent8-cert.pem');
+const ca = loadKey('fake-startcom-root-cert.pem');
+
+const server = http2.createSecureServer({key, cert});
+
+server.on('stream', (stream, headers) => {
+ const name = headers[HTTP2_HEADER_PATH].slice(1);
+ const file = path.resolve(common.fixturesDir, name);
+ fs.stat(file, (err, stat) => {
+ if (err != null || stat.isDirectory()) {
+ stream.respond({ [HTTP2_HEADER_STATUS]: 404 });
+ stream.end();
+ } else {
+ stream.respond({ [HTTP2_HEADER_STATUS]: 200 });
+ const str = fs.createReadStream(file);
+ str.pipe(stream);
+ }
+ });
+});
+
+server.listen(8000, () => {
+
+ const secureContext = tls.createSecureContext({ca});
+ const client = http2.connect(`https://localhost:${server.address().port}`,
+ { secureContext });
+
+ let remaining = 2;
+ function maybeClose() {
+ if (--remaining === 0) {
+ client.destroy();
+ server.close();
+ }
+ }
+
+ // Request for a file that does exist, response is 200
+ const req1 = client.request({ [HTTP2_HEADER_PATH]: '/a.js' },
+ { endStream: true });
+ req1.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200);
+ }));
+ let req1_data = '';
+ req1.setEncoding('utf8');
+ req1.on('data', (chunk) => req1_data += chunk);
+ req1.on('end', common.mustCall(() => {
+ assert.strictEqual(req1_data, ajs_data);
+ maybeClose();
+ }));
+
+ // Request for a file that does not exist, response is 404
+ const req2 = client.request({ [HTTP2_HEADER_PATH]: '/does_not_exist' },
+ { endStream: true });
+ req2.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[HTTP2_HEADER_STATUS], 404);
+ }));
+ req2.on('data', common.mustNotCall());
+ req2.on('end', common.mustCall(() => maybeClose()));
+
+});
diff --git a/test/parallel/test-http2-server-destroy-before-additional.js b/test/parallel/test-http2-server-destroy-before-additional.js
new file mode 100644
index 00000000000000..9aff3b6abf987b
--- /dev/null
+++ b/test/parallel/test-http2-server-destroy-before-additional.js
@@ -0,0 +1,38 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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.session.destroy();
+ assert.throws(() => stream.additionalHeaders({}),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_STREAM',
+ message: /^The stream has been destroyed$/
+ }));
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustNotCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-server-destroy-before-push.js b/test/parallel/test-http2-server-destroy-before-push.js
new file mode 100644
index 00000000000000..9c1628a18bf671
--- /dev/null
+++ b/test/parallel/test-http2-server-destroy-before-push.js
@@ -0,0 +1,38 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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.session.destroy();
+ assert.throws(() => stream.pushStream({}, common.mustNotCall()),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_STREAM',
+ message: /^The stream has been destroyed$/
+ }));
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustNotCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-server-destroy-before-respond.js b/test/parallel/test-http2-server-destroy-before-respond.js
new file mode 100644
index 00000000000000..acb020d5bd7e3e
--- /dev/null
+++ b/test/parallel/test-http2-server-destroy-before-respond.js
@@ -0,0 +1,38 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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.session.destroy();
+ assert.throws(() => stream.respond({}),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_STREAM',
+ message: /^The stream has been destroyed$/
+ }));
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustNotCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-server-destroy-before-write.js b/test/parallel/test-http2-server-destroy-before-write.js
new file mode 100644
index 00000000000000..533aace208c44b
--- /dev/null
+++ b/test/parallel/test-http2-server-destroy-before-write.js
@@ -0,0 +1,38 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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.session.destroy();
+ assert.throws(() => stream.write('data'),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_STREAM',
+ type: Error
+ }));
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('response', common.mustNotCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-server-push-disabled.js b/test/parallel/test-http2-server-push-disabled.js
new file mode 100644
index 00000000000000..1fedf2229378ef
--- /dev/null
+++ b/test/parallel/test-http2-server-push-disabled.js
@@ -0,0 +1,53 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+server.on('session', common.mustCall((session) => {
+ // Verify that the settings disabling push is received
+ session.on('remoteSettings', common.mustCall((settings) => {
+ assert.strictEqual(settings.enablePush, false);
+ }));
+}));
+
+server.on('stream', common.mustCall((stream) => {
+
+ // The client has disabled push streams, so pushAllowed must be false,
+ // and pushStream() must throw.
+ assert.strictEqual(stream.pushAllowed, false);
+
+ assert.throws(() => {
+ stream.pushStream({
+ ':scheme': 'http',
+ ':path': '/foobar',
+ ':authority': `localhost:${server.address().port}`,
+ }, common.mustNotCall());
+ }, common.expectsError({
+ code: 'ERR_HTTP2_PUSH_DISABLED',
+ type: Error
+ }));
+
+ stream.respond({ ':status': 200 });
+ stream.end('test');
+}));
+
+server.listen(0, common.mustCall(() => {
+ const options = {settings: { enablePush: false }};
+ const client = http2.connect(`http://localhost:${server.address().port}`,
+ options);
+ const req = client.request({ ':path': '/' });
+
+ // Because push stream sre disabled, this must not be called.
+ client.on('stream', common.mustNotCall());
+
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+}));
diff --git a/test/parallel/test-http2-server-push-stream.js b/test/parallel/test-http2-server-push-stream.js
new file mode 100644
index 00000000000000..c2f34ed517c6c9
--- /dev/null
+++ b/test/parallel/test-http2-server-push-stream.js
@@ -0,0 +1,58 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream, headers) => {
+ const port = server.address().port;
+ if (headers[':path'] === '/') {
+ stream.pushStream({
+ ':scheme': 'http',
+ ':path': '/foobar',
+ ':authority': `localhost:${port}`,
+ }, (push, headers) => {
+ push.respond({
+ 'content-type': 'text/html',
+ ':status': 200,
+ 'x-push-data': 'pushed by server',
+ });
+ push.end('pushed by server data');
+ stream.end('test');
+ });
+ }
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+}));
+
+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);
+
+ client.on('stream', common.mustCall((stream, headers) => {
+ assert.strictEqual(headers[':scheme'], 'http');
+ assert.strictEqual(headers[':path'], '/foobar');
+ assert.strictEqual(headers[':authority'], `localhost:${port}`);
+ stream.on('push', common.mustCall((headers) => {
+ assert.strictEqual(headers[':status'], 200);
+ assert.strictEqual(headers['content-type'], 'text/html');
+ assert.strictEqual(headers['x-push-data'], 'pushed by server');
+ }));
+ }));
+
+ let data = '';
+
+ req.on('data', common.mustCall((d) => data += d));
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(data, 'test');
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+}));
diff --git a/test/parallel/test-http2-server-rst-before-respond.js b/test/parallel/test-http2-server-rst-before-respond.js
new file mode 100644
index 00000000000000..015e11311f7544
--- /dev/null
+++ b/test/parallel/test-http2-server-rst-before-respond.js
@@ -0,0 +1,45 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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.rstStream();
+
+ assert.throws(() => {
+ stream.additionalHeaders({
+ ':status': 123,
+ abc: 123
+ });
+ }, common.expectsError({
+ code: 'ERR_HTTP2_INVALID_STREAM',
+ message: /^The stream has been destroyed$/
+ }));
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.on('headers', common.mustNotCall());
+
+ req.on('streamClosed', common.mustCall((code) => {
+ assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, code);
+ server.close();
+ client.destroy();
+ }));
+
+ 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
new file mode 100644
index 00000000000000..30a9db49afc239
--- /dev/null
+++ b/test/parallel/test-http2-server-rst-stream.js
@@ -0,0 +1,72 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const {
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_METHOD_POST,
+ NGHTTP2_CANCEL,
+ NGHTTP2_NO_ERROR,
+ NGHTTP2_PROTOCOL_ERROR,
+ NGHTTP2_REFUSED_STREAM,
+ NGHTTP2_INTERNAL_ERROR
+} = http2.constants;
+
+const errCheck = common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR' }, 8);
+
+function checkRstCode(rstMethod, expectRstCode) {
+ const server = http2.createServer();
+ server.on('stream', (stream, headers, flags) => {
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.write('test');
+ if (rstMethod === 'rstStream')
+ stream[rstMethod](expectRstCode);
+ else
+ stream[rstMethod]();
+
+ if (expectRstCode > NGHTTP2_NO_ERROR) {
+ stream.on('error', common.mustCall(errCheck));
+ }
+ });
+
+ server.listen(0, common.mustCall(() => {
+ const port = server.address().port;
+ const client = http2.connect(`http://localhost:${port}`);
+
+ const headers = {
+ [HTTP2_HEADER_PATH]: '/',
+ [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST
+ };
+ const req = client.request(headers);
+
+ req.setEncoding('utf8');
+ req.on('streamClosed', common.mustCall((actualRstCode) => {
+ assert.strictEqual(
+ expectRstCode, actualRstCode, `${rstMethod} is not match rstCode`);
+ server.close();
+ client.destroy();
+ }));
+ req.on('data', common.mustCall());
+ req.on('aborted', common.mustCall());
+ req.on('end', common.mustCall());
+
+ if (expectRstCode > NGHTTP2_NO_ERROR) {
+ req.on('error', common.mustCall(errCheck));
+ }
+
+ }));
+}
+
+checkRstCode('rstStream', NGHTTP2_NO_ERROR);
+checkRstCode('rstWithNoError', NGHTTP2_NO_ERROR);
+checkRstCode('rstWithProtocolError', NGHTTP2_PROTOCOL_ERROR);
+checkRstCode('rstWithCancel', NGHTTP2_CANCEL);
+checkRstCode('rstWithRefuse', NGHTTP2_REFUSED_STREAM);
+checkRstCode('rstWithInternalError', NGHTTP2_INTERNAL_ERROR);
diff --git a/test/parallel/test-http2-server-set-header.js b/test/parallel/test-http2-server-set-header.js
new file mode 100644
index 00000000000000..8b0e82fd8a50d5
--- /dev/null
+++ b/test/parallel/test-http2-server-set-header.js
@@ -0,0 +1,36 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+const body =
+ 'this is some data';
+
+const server = http2.createServer((req, res) => {
+ res.setHeader('foobar', 'baz');
+ res.setHeader('X-POWERED-BY', 'node-test');
+ res.end(body);
+});
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const headers = { ':path': '/' };
+ const req = client.request(headers);
+ req.setEncoding('utf8');
+ req.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers['foobar'], 'baz');
+ assert.strictEqual(headers['x-powered-by'], 'node-test');
+ }));
+
+ let data = '';
+ req.on('data', (d) => data += d);
+ req.on('end', () => {
+ assert.strictEqual(body, data);
+ server.close();
+ client.destroy();
+ });
+ req.end();
+}));
+
+server.on('error', common.mustNotCall());
diff --git a/test/parallel/test-http2-server-shutdown-before-respond.js b/test/parallel/test-http2-server-shutdown-before-respond.js
new file mode 100644
index 00000000000000..c7ffee7e31532e
--- /dev/null
+++ b/test/parallel/test-http2-server-shutdown-before-respond.js
@@ -0,0 +1,32 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+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) {
+ const session = stream.session;
+ stream.session.shutdown({graceful: true}, common.mustCall(() => {
+ session.destroy();
+ }));
+ stream.respond({});
+ stream.end('data');
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({ ':path': '/' });
+
+ req.resume();
+ req.on('end', common.mustCall(() => server.close()));
+ req.end();
+}));
diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js
new file mode 100644
index 00000000000000..c10bbd0ccbe0c5
--- /dev/null
+++ b/test/parallel/test-http2-server-socket-destroy.js
@@ -0,0 +1,57 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const h2 = require('http2');
+const assert = require('assert');
+
+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.write('test');
+
+ const socket = stream.session.socket;
+
+ // When the socket is destroyed, the close events must be triggered
+ // on the socket, server and session.
+ socket.on('close', common.mustCall());
+ server.on('close', common.mustCall());
+ stream.session.on('close', common.mustCall(() => server.close()));
+
+ // Also, the aborted event must be triggered on the stream
+ stream.on('aborted', common.mustCall());
+
+ assert.notStrictEqual(stream.session, undefined);
+ socket.destroy();
+ assert.strictEqual(stream.session, undefined);
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const req = client.request({
+ [HTTP2_HEADER_PATH]: '/',
+ [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST });
+
+ req.on('aborted', common.mustCall());
+ req.on('end', common.mustCall());
+ req.on('response', common.mustCall());
+ req.on('data', common.mustCall());
+
+ client.on('close', common.mustCall());
+}));
diff --git a/test/parallel/test-http2-server-startup.js b/test/parallel/test-http2-server-startup.js
new file mode 100644
index 00000000000000..c2e94f3ac4502a
--- /dev/null
+++ b/test/parallel/test-http2-server-startup.js
@@ -0,0 +1,78 @@
+// Flags: --expose-http2
+'use strict';
+
+// Tests the basic operation of creating a plaintext or TLS
+// HTTP2 server. The server does not do anything at this point
+// other than start listening.
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+const path = require('path');
+const tls = require('tls');
+const net = require('net');
+const fs = require('fs');
+
+const options = {
+ key: fs.readFileSync(
+ path.resolve(common.fixturesDir, 'keys/agent2-key.pem')),
+ cert: fs.readFileSync(
+ path.resolve(common.fixturesDir, 'keys/agent2-cert.pem'))
+};
+
+// There should not be any throws
+assert.doesNotThrow(() => {
+
+ const serverTLS = http2.createSecureServer(options, () => {});
+
+ serverTLS.listen(0, common.mustCall(() => serverTLS.close()));
+
+ // There should not be an error event reported either
+ serverTLS.on('error', common.mustNotCall());
+});
+
+// There should not be any throws
+assert.doesNotThrow(() => {
+ const server = http2.createServer(options, common.mustNotCall());
+
+ server.listen(0, common.mustCall(() => server.close()));
+
+ // There should not be an error event reported either
+ server.on('error', common.mustNotCall());
+});
+
+// Test the plaintext server socket timeout
+{
+ let client;
+ const server = http2.createServer();
+ server.on('timeout', common.mustCall(() => {
+ server.close();
+ if (client)
+ client.end();
+ }));
+ server.setTimeout(common.platformTimeout(1000));
+ server.listen(0, common.mustCall(() => {
+ const port = server.address().port;
+ client = net.connect(port, common.mustCall());
+ }));
+}
+
+// Test the secure server socket timeout
+{
+ let client;
+ const server = http2.createSecureServer(options);
+ server.on('timeout', common.mustCall(() => {
+ server.close();
+ if (client)
+ client.end();
+ }));
+ server.setTimeout(common.platformTimeout(1000));
+ server.listen(0, common.mustCall(() => {
+ const port = server.address().port;
+ client = tls.connect({
+ port: port,
+ rejectUnauthorized: false,
+ ALPNProtocols: ['h2']
+ }, common.mustCall());
+ }));
+}
diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js
new file mode 100644
index 00000000000000..bc9877e23e38f6
--- /dev/null
+++ b/test/parallel/test-http2-session-settings.js
@@ -0,0 +1,110 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+server.on('stream', common.mustCall(onStream));
+
+function assertSettings(settings) {
+ assert.strictEqual(typeof settings, 'object');
+ assert.strictEqual(typeof settings.headerTableSize, 'number');
+ assert.strictEqual(typeof settings.enablePush, 'boolean');
+ assert.strictEqual(typeof settings.initialWindowSize, 'number');
+ assert.strictEqual(typeof settings.maxFrameSize, 'number');
+ assert.strictEqual(typeof settings.maxConcurrentStreams, 'number');
+ assert.strictEqual(typeof settings.maxHeaderListSize, 'number');
+}
+
+function onStream(stream, headers, flags) {
+
+ assertSettings(stream.session.localSettings);
+ assertSettings(stream.session.remoteSettings);
+
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end('hello world');
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`, {
+ settings: {
+ enablePush: false,
+ initialWindowSize: 123456
+ }
+ });
+
+ client.on('localSettings', common.mustCall((settings) => {
+ assert(settings);
+ assert.strictEqual(settings.enablePush, false);
+ assert.strictEqual(settings.initialWindowSize, 123456);
+ assert.strictEqual(settings.maxFrameSize, 16384);
+ }, 2));
+ client.on('remoteSettings', common.mustCall((settings) => {
+ assert(settings);
+ }));
+
+ const headers = { ':path': '/' };
+
+ 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);
+ }));
+
+ // State will only be valid after connect event is emitted
+ req.on('ready', common.mustCall(() => {
+ assert.doesNotThrow(() => {
+ client.settings({
+ maxHeaderListSize: 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];
+ assert.throws(() => client.settings(settings),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+ type: RangeError
+ }));
+ });
+ [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => {
+ assert.throws(() => client.settings({enablePush: i}),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+ type: TypeError
+ }));
+ });
+
+ }));
+
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-session-stream-state.js b/test/parallel/test-http2-session-stream-state.js
new file mode 100644
index 00000000000000..9ba56f958c43da
--- /dev/null
+++ b/test/parallel/test-http2-session-stream-state.js
@@ -0,0 +1,97 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+server.on('stream', common.mustCall(onStream));
+
+function onStream(stream, headers, flags) {
+
+ // Test Stream State.
+ {
+ const state = stream.state;
+ assert.strictEqual(typeof state, 'object');
+ assert.strictEqual(typeof state.state, 'number');
+ assert.strictEqual(typeof state.weight, 'number');
+ assert.strictEqual(typeof state.sumDependencyWeight, 'number');
+ assert.strictEqual(typeof state.localClose, 'number');
+ assert.strictEqual(typeof state.remoteClose, 'number');
+ assert.strictEqual(typeof state.localWindowSize, 'number');
+ }
+
+ // Test Session State.
+ {
+ const state = stream.session.state;
+ assert.strictEqual(typeof state, 'object');
+ assert.strictEqual(typeof state.effectiveLocalWindowSize, 'number');
+ assert.strictEqual(typeof state.effectiveRecvDataLength, 'number');
+ assert.strictEqual(typeof state.nextStreamID, 'number');
+ assert.strictEqual(typeof state.localWindowSize, 'number');
+ assert.strictEqual(typeof state.lastProcStreamID, 'number');
+ assert.strictEqual(typeof state.remoteWindowSize, 'number');
+ assert.strictEqual(typeof state.outboundQueueSize, 'number');
+ assert.strictEqual(typeof state.deflateDynamicTableSize, 'number');
+ assert.strictEqual(typeof state.inflateDynamicTableSize, 'number');
+ }
+
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end('hello world');
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+
+ const headers = { ':path': '/' };
+
+ const req = client.request(headers);
+
+ // State will only be valid after connect event is emitted
+ req.on('connect', common.mustCall(() => {
+
+ // Test Stream State.
+ {
+ const state = req.state;
+ assert.strictEqual(typeof state, 'object');
+ assert.strictEqual(typeof state.state, 'number');
+ assert.strictEqual(typeof state.weight, 'number');
+ assert.strictEqual(typeof state.sumDependencyWeight, 'number');
+ assert.strictEqual(typeof state.localClose, 'number');
+ assert.strictEqual(typeof state.remoteClose, 'number');
+ assert.strictEqual(typeof state.localWindowSize, 'number');
+ }
+
+ // Test Session State
+ {
+ const state = req.session.state;
+ assert.strictEqual(typeof state, 'object');
+ assert.strictEqual(typeof state.effectiveLocalWindowSize, 'number');
+ assert.strictEqual(typeof state.effectiveRecvDataLength, 'number');
+ assert.strictEqual(typeof state.nextStreamID, 'number');
+ assert.strictEqual(typeof state.localWindowSize, 'number');
+ assert.strictEqual(typeof state.lastProcStreamID, 'number');
+ assert.strictEqual(typeof state.remoteWindowSize, 'number');
+ assert.strictEqual(typeof state.outboundQueueSize, 'number');
+ assert.strictEqual(typeof state.deflateDynamicTableSize, 'number');
+ assert.strictEqual(typeof state.inflateDynamicTableSize, 'number');
+ }
+ }));
+
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-single-headers.js b/test/parallel/test-http2-single-headers.js
new file mode 100644
index 00000000000000..49918acc474bcb
--- /dev/null
+++ b/test/parallel/test-http2-single-headers.js
@@ -0,0 +1,59 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+// Each of these headers must appear only once
+const singles = [
+ 'content-type',
+ 'user-agent',
+ 'referer',
+ 'authorization',
+ 'proxy-authorization',
+ 'if-modified-since',
+ 'if-unmodified-since',
+ 'from',
+ 'location',
+ 'max-forwards'
+];
+
+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));
+ });
+
+}));
diff --git a/test/parallel/test-http2-status-code-invalid.js b/test/parallel/test-http2-status-code-invalid.js
new file mode 100644
index 00000000000000..cb8c9072f73c3b
--- /dev/null
+++ b/test/parallel/test-http2-status-code-invalid.js
@@ -0,0 +1,40 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+function expectsError(code) {
+ return common.expectsError({
+ code: 'ERR_HTTP2_STATUS_INVALID',
+ type: RangeError,
+ message: `Invalid status code: ${code}`
+ });
+}
+
+server.on('stream', common.mustCall((stream) => {
+
+ // Anything lower than 100 and greater than 599 is rejected
+ [ 99, 700, 1000 ].forEach((i) => {
+ assert.throws(() => stream.respond({ ':status': i }), expectsError(i));
+ });
+
+ stream.respond();
+ stream.end();
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request();
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[':status'], 200);
+ }));
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+}));
diff --git a/test/parallel/test-http2-status-code.js b/test/parallel/test-http2-status-code.js
new file mode 100644
index 00000000000000..f094d981c36eec
--- /dev/null
+++ b/test/parallel/test-http2-status-code.js
@@ -0,0 +1,40 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const codes = [ 200, 202, 300, 400, 404, 451, 500 ];
+let test = 0;
+
+const server = http2.createServer();
+
+server.on('stream', common.mustCall((stream) => {
+ const status = codes[test++];
+ stream.respond({ ':status': status }, { endStream: true });
+}, 7));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ let remaining = codes.length;
+ function maybeClose() {
+ if (--remaining === 0) {
+ client.destroy();
+ server.close();
+ }
+ }
+
+ function doTest(expected) {
+ const req = client.request();
+ req.on('response', common.mustCall((headers) => {
+ assert.strictEqual(headers[':status'], expected);
+ }));
+ req.resume();
+ req.on('end', common.mustCall(maybeClose));
+ }
+
+ for (let n = 0; n < codes.length; n++)
+ doTest(codes[n]);
+}));
diff --git a/test/parallel/test-http2-timeouts.js b/test/parallel/test-http2-timeouts.js
new file mode 100644
index 00000000000000..132496e1fcdc33
--- /dev/null
+++ b/test/parallel/test-http2-timeouts.js
@@ -0,0 +1,32 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+// we use the lower-level API here
+server.on('stream', common.mustCall((stream) => {
+ stream.setTimeout(1, common.mustCall(() => {
+ stream.respond({':status': 200});
+ stream.end('hello world');
+ }));
+}));
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+ const client = h2.connect(`http://localhost:${server.address().port}`);
+ client.setTimeout(1, common.mustCall(() => {
+ const req = client.request({ ':path': '/' });
+ req.setTimeout(1, common.mustCall(() => {
+ req.on('response', common.mustCall());
+ req.resume();
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+ }));
+ }));
+}));
diff --git a/test/parallel/test-http2-too-many-settings.js b/test/parallel/test-http2-too-many-settings.js
new file mode 100644
index 00000000000000..4a64645df14174
--- /dev/null
+++ b/test/parallel/test-http2-too-many-settings.js
@@ -0,0 +1,60 @@
+// Flags: --expose-http2
+'use strict';
+
+// Tests that attempting to send too many non-acknowledged
+// settings frames will result in a throw.
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+
+const maxPendingAck = 2;
+const server = h2.createServer({ maxPendingAck: maxPendingAck + 1 });
+
+let clients = 2;
+
+function doTest(session) {
+ for (let n = 0; n < maxPendingAck; n++)
+ assert.doesNotThrow(() => session.settings({ enablePush: false }));
+ assert.throws(() => session.settings({ enablePush: false }),
+ common.expectsError({
+ code: 'ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
+ type: Error
+ }));
+}
+
+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)));
+}));
+
+// Setting maxPendingAck to 0, defaults it to 1
+server.on('listening', common.mustCall(() => {
+ const client = h2.connect(`http://localhost:${server.address().port}`,
+ { maxPendingAck: 0 });
+
+ client.on('close', closeServer);
+ client.on('localSettings', common.mustCall(() => {
+ client.destroy();
+ }));
+}));
diff --git a/test/parallel/test-http2-trailers.js b/test/parallel/test-http2-trailers.js
new file mode 100644
index 00000000000000..35a5dbf28a1289
--- /dev/null
+++ b/test/parallel/test-http2-trailers.js
@@ -0,0 +1,44 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const h2 = require('http2');
+const body =
+ 'this is some data';
+const trailerKey = 'test-trailer';
+const trailerValue = 'testing';
+
+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.on('fetchTrailers', function(trailers) {
+ trailers[trailerKey] = trailerValue;
+ });
+ stream.end(body);
+}
+
+server.listen(0);
+
+server.on('listening', common.mustCall(function() {
+ const client = h2.connect(`http://localhost:${this.address().port}`);
+ const req = client.request({':path': '/'});
+ req.on('data', common.mustCall());
+ req.on('trailers', common.mustCall((headers) => {
+ assert.strictEqual(headers[trailerKey], trailerValue);
+ req.end();
+ }));
+ req.on('end', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ req.end();
+
+}));
diff --git a/test/parallel/test-http2-util-asserts.js b/test/parallel/test-http2-util-asserts.js
new file mode 100644
index 00000000000000..fd902f01a3d29a
--- /dev/null
+++ b/test/parallel/test-http2-util-asserts.js
@@ -0,0 +1,43 @@
+// Flags: --expose-internals --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const {
+ assertIsObject,
+ assertWithinRange,
+} = require('internal/http2/util');
+
+[
+ undefined,
+ {},
+ Object.create(null),
+ new Date(),
+ new (class Foo {})()
+].forEach((i) => {
+ assert.doesNotThrow(() => assertIsObject(i, 'foo', 'object'));
+});
+
+[
+ 1,
+ false,
+ 'hello',
+ NaN,
+ Infinity,
+ [],
+ [{}]
+].forEach((i) => {
+ assert.throws(() => assertIsObject(i, 'foo', 'object'),
+ common.expectsError({
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "foo" argument must be of type object$/
+ }));
+});
+
+assert.doesNotThrow(() => assertWithinRange('foo', 1, 0, 2));
+
+assert.throws(() => assertWithinRange('foo', 1, 2, 3),
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+ message: /^Invalid value for setting "foo": 1$/
+ }));
diff --git a/test/parallel/test-http2-util-headers-list.js b/test/parallel/test-http2-util-headers-list.js
new file mode 100644
index 00000000000000..d19c78a2b3c3ff
--- /dev/null
+++ b/test/parallel/test-http2-util-headers-list.js
@@ -0,0 +1,248 @@
+// Flags: --expose-internals --expose-http2
+'use strict';
+
+// Tests the internal utility function that is used to prepare headers
+// to pass to the internal binding layer.
+
+const common = require('../common');
+const assert = require('assert');
+const { mapToHeaders } = require('internal/http2/util');
+
+const {
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_SCHEME,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_AGE,
+ HTTP2_HEADER_AUTHORIZATION,
+ HTTP2_HEADER_CONTENT_ENCODING,
+ HTTP2_HEADER_CONTENT_LANGUAGE,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_CONTENT_LOCATION,
+ HTTP2_HEADER_CONTENT_MD5,
+ HTTP2_HEADER_CONTENT_RANGE,
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_DATE,
+ HTTP2_HEADER_ETAG,
+ HTTP2_HEADER_EXPIRES,
+ HTTP2_HEADER_FROM,
+ HTTP2_HEADER_IF_MATCH,
+ HTTP2_HEADER_IF_MODIFIED_SINCE,
+ HTTP2_HEADER_IF_NONE_MATCH,
+ HTTP2_HEADER_IF_RANGE,
+ HTTP2_HEADER_IF_UNMODIFIED_SINCE,
+ HTTP2_HEADER_LAST_MODIFIED,
+ HTTP2_HEADER_MAX_FORWARDS,
+ HTTP2_HEADER_PROXY_AUTHORIZATION,
+ HTTP2_HEADER_RANGE,
+ HTTP2_HEADER_REFERER,
+ HTTP2_HEADER_RETRY_AFTER,
+ HTTP2_HEADER_USER_AGENT,
+
+ HTTP2_HEADER_ACCEPT_CHARSET,
+ HTTP2_HEADER_ACCEPT_ENCODING,
+ HTTP2_HEADER_ACCEPT_LANGUAGE,
+ HTTP2_HEADER_ACCEPT_RANGES,
+ HTTP2_HEADER_ACCEPT,
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
+ HTTP2_HEADER_ALLOW,
+ HTTP2_HEADER_CACHE_CONTROL,
+ HTTP2_HEADER_CONTENT_DISPOSITION,
+ HTTP2_HEADER_COOKIE,
+ HTTP2_HEADER_EXPECT,
+ HTTP2_HEADER_LINK,
+ HTTP2_HEADER_PREFER,
+ HTTP2_HEADER_PROXY_AUTHENTICATE,
+ HTTP2_HEADER_REFRESH,
+ HTTP2_HEADER_SERVER,
+ HTTP2_HEADER_SET_COOKIE,
+ HTTP2_HEADER_STRICT_TRANSPORT_SECURITY,
+ HTTP2_HEADER_VARY,
+ HTTP2_HEADER_VIA,
+ HTTP2_HEADER_WWW_AUTHENTICATE,
+
+ HTTP2_HEADER_CONNECTION,
+ HTTP2_HEADER_UPGRADE,
+ HTTP2_HEADER_HTTP2_SETTINGS,
+ HTTP2_HEADER_TE,
+ HTTP2_HEADER_TRANSFER_ENCODING,
+ HTTP2_HEADER_HOST,
+ HTTP2_HEADER_KEEP_ALIVE,
+ HTTP2_HEADER_PROXY_CONNECTION
+} = process.binding('http2').constants;
+
+{
+ const headers = {
+ 'abc': 1,
+ ':status': 200,
+ ':path': 'abc',
+ 'xyz': [1, '2', { toString() { return '3'; } }, 4],
+ 'foo': [],
+ 'BAR': [1]
+ };
+
+ assert.deepStrictEqual(mapToHeaders(headers), [
+ [ ':path', 'abc' ],
+ [ ':status', '200' ],
+ [ 'abc', '1' ],
+ [ 'xyz', '1' ],
+ [ 'xyz', '2' ],
+ [ 'xyz', '3' ],
+ [ 'xyz', '4' ],
+ [ 'bar', '1' ]
+ ]);
+}
+
+{
+ const headers = {
+ 'abc': 1,
+ ':path': 'abc',
+ ':status': [200],
+ ':authority': [],
+ 'xyz': [1, 2, 3, 4]
+ };
+
+ assert.deepStrictEqual(mapToHeaders(headers), [
+ [ ':status', '200' ],
+ [ ':path', 'abc' ],
+ [ 'abc', '1' ],
+ [ 'xyz', '1' ],
+ [ 'xyz', '2' ],
+ [ 'xyz', '3' ],
+ [ 'xyz', '4' ]
+ ]);
+}
+
+{
+ const headers = {
+ 'abc': 1,
+ ':path': 'abc',
+ 'xyz': [1, 2, 3, 4],
+ '': 1,
+ ':status': 200,
+ [Symbol('test')]: 1 // Symbol keys are ignored
+ };
+
+ assert.deepStrictEqual(mapToHeaders(headers), [
+ [ ':status', '200' ],
+ [ ':path', 'abc' ],
+ [ 'abc', '1' ],
+ [ 'xyz', '1' ],
+ [ 'xyz', '2' ],
+ [ 'xyz', '3' ],
+ [ 'xyz', '4' ]
+ ]);
+}
+
+{
+ // Only own properties are used
+ const base = { 'abc': 1 };
+ const headers = Object.create(base);
+ headers[':path'] = 'abc';
+ headers.xyz = [1, 2, 3, 4];
+ headers.foo = [];
+ headers[':status'] = 200;
+
+ assert.deepStrictEqual(mapToHeaders(headers), [
+ [ ':status', '200' ],
+ [ ':path', 'abc' ],
+ [ 'xyz', '1' ],
+ [ 'xyz', '2' ],
+ [ 'xyz', '3' ],
+ [ 'xyz', '4' ]
+ ]);
+}
+
+// The following are not allowed to have multiple values
+[
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_SCHEME,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_AGE,
+ HTTP2_HEADER_AUTHORIZATION,
+ HTTP2_HEADER_CONTENT_ENCODING,
+ HTTP2_HEADER_CONTENT_LANGUAGE,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_CONTENT_LOCATION,
+ HTTP2_HEADER_CONTENT_MD5,
+ HTTP2_HEADER_CONTENT_RANGE,
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_DATE,
+ HTTP2_HEADER_ETAG,
+ HTTP2_HEADER_EXPIRES,
+ HTTP2_HEADER_FROM,
+ HTTP2_HEADER_IF_MATCH,
+ HTTP2_HEADER_IF_MODIFIED_SINCE,
+ HTTP2_HEADER_IF_NONE_MATCH,
+ HTTP2_HEADER_IF_RANGE,
+ HTTP2_HEADER_IF_UNMODIFIED_SINCE,
+ HTTP2_HEADER_LAST_MODIFIED,
+ HTTP2_HEADER_MAX_FORWARDS,
+ HTTP2_HEADER_PROXY_AUTHORIZATION,
+ HTTP2_HEADER_RANGE,
+ HTTP2_HEADER_REFERER,
+ HTTP2_HEADER_RETRY_AFTER,
+ HTTP2_HEADER_USER_AGENT
+].forEach((name) => {
+ const msg = `Header field "${name}" must have only a single value`;
+ common.expectsError({
+ code: 'ERR_HTTP2_HEADER_SINGLE_VALUE',
+ message: msg
+ })(mapToHeaders({[name]: [1, 2, 3]}));
+});
+
+[
+ HTTP2_HEADER_ACCEPT_CHARSET,
+ HTTP2_HEADER_ACCEPT_ENCODING,
+ HTTP2_HEADER_ACCEPT_LANGUAGE,
+ HTTP2_HEADER_ACCEPT_RANGES,
+ HTTP2_HEADER_ACCEPT,
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
+ HTTP2_HEADER_ALLOW,
+ HTTP2_HEADER_CACHE_CONTROL,
+ HTTP2_HEADER_CONTENT_DISPOSITION,
+ HTTP2_HEADER_COOKIE,
+ HTTP2_HEADER_EXPECT,
+ HTTP2_HEADER_LINK,
+ HTTP2_HEADER_PREFER,
+ HTTP2_HEADER_PROXY_AUTHENTICATE,
+ HTTP2_HEADER_REFRESH,
+ HTTP2_HEADER_SERVER,
+ HTTP2_HEADER_SET_COOKIE,
+ HTTP2_HEADER_STRICT_TRANSPORT_SECURITY,
+ HTTP2_HEADER_VARY,
+ HTTP2_HEADER_VIA,
+ HTTP2_HEADER_WWW_AUTHENTICATE
+].forEach((name) => {
+ assert(!(mapToHeaders({[name]: [1, 2, 3]}) instanceof Error), name);
+});
+
+const regex =
+ /^HTTP\/1 Connection specific headers are forbidden$/;
+[
+ HTTP2_HEADER_CONNECTION,
+ HTTP2_HEADER_UPGRADE,
+ HTTP2_HEADER_HTTP2_SETTINGS,
+ HTTP2_HEADER_TE,
+ HTTP2_HEADER_TRANSFER_ENCODING,
+ HTTP2_HEADER_HOST,
+ HTTP2_HEADER_PROXY_CONNECTION,
+ HTTP2_HEADER_KEEP_ALIVE,
+ 'Connection',
+ 'Upgrade',
+ 'HTTP2-Settings',
+ 'TE',
+ 'Transfer-Encoding',
+ 'Proxy-Connection',
+ 'Keep-Alive'
+].forEach((name) => {
+ common.expectsError({
+ code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS',
+ message: regex
+ })(mapToHeaders({[name]: 'abc'}));
+});
+
+assert(!(mapToHeaders({ te: 'trailers' }) instanceof Error));
diff --git a/test/parallel/test-http2-window-size.js b/test/parallel/test-http2-window-size.js
new file mode 100644
index 00000000000000..d914e99f6aa195
--- /dev/null
+++ b/test/parallel/test-http2-window-size.js
@@ -0,0 +1,102 @@
+// Flags: --expose-http2
+'use strict';
+
+// This test ensures that servers are able to send data independent of window
+// size.
+// TODO: This test makes large buffer allocations (128KiB) and should be tested
+// on smaller / IoT platforms in case this poses problems for these targets.
+
+const assert = require('assert');
+const common = require('../common');
+const h2 = require('http2');
+
+// Given a list of buffers and an initial window size, have a server write
+// each buffer to the HTTP2 Writable stream, and let the client verify that
+// all of the bytes were sent correctly
+function run(buffers, initialWindowSize) {
+ return new Promise((resolve, reject) => {
+ const expectedBuffer = Buffer.concat(buffers);
+
+ const server = h2.createServer();
+ server.on('stream', (stream) => {
+ let i = 0;
+ const writeToStream = () => {
+ const cont = () => {
+ i++;
+ if (i < buffers.length) {
+ setImmediate(writeToStream);
+ } else {
+ stream.end();
+ }
+ };
+ const drained = stream.write(buffers[i]);
+ if (drained) {
+ cont();
+ } else {
+ stream.once('drain', cont);
+ }
+ };
+ writeToStream();
+ });
+ server.listen(0);
+
+ server.on('listening', common.mustCall(function() {
+ const port = this.address().port;
+
+ const client =
+ h2.connect({
+ authority: 'localhost',
+ protocol: 'http:',
+ port
+ }, {
+ settings: {
+ initialWindowSize
+ }
+ }).on('connect', common.mustCall(() => {
+ const req = client.request({
+ ':method': 'GET',
+ ':path': '/'
+ });
+ const responses = [];
+ req.on('data', (data) => {
+ responses.push(data);
+ });
+ req.on('end', common.mustCall(() => {
+ const actualBuffer = Buffer.concat(responses);
+ assert.strictEqual(Buffer.compare(actualBuffer, expectedBuffer), 0);
+ // shut down
+ client.destroy();
+ server.close(() => {
+ resolve();
+ });
+ }));
+ req.end();
+ }));
+ }));
+ });
+}
+
+const bufferValueRange = [0, 1, 2, 3];
+const buffersList = [
+ bufferValueRange.map((a) => Buffer.alloc(1 << 4, a)),
+ bufferValueRange.map((a) => Buffer.alloc((1 << 8) - 1, a)),
+// Specifying too large of a value causes timeouts on some platforms
+// bufferValueRange.map((a) => Buffer.alloc(1 << 17, a))
+];
+const initialWindowSizeList = [
+ 1 << 4,
+ (1 << 8) - 1,
+ 1 << 8,
+ 1 << 17,
+ undefined // use default window size which is (1 << 16) - 1
+];
+
+// Call `run` on each element in the cartesian product of buffersList and
+// initialWindowSizeList.
+let p = Promise.resolve();
+for (const buffers of buffersList) {
+ for (const initialWindowSize of initialWindowSizeList) {
+ p = p.then(() => run(buffers, initialWindowSize));
+ }
+}
+p.then(common.mustCall(() => {}));
diff --git a/test/parallel/test-http2-withflag.js b/test/parallel/test-http2-withflag.js
new file mode 100644
index 00000000000000..557ec40e643eba
--- /dev/null
+++ b/test/parallel/test-http2-withflag.js
@@ -0,0 +1,7 @@
+// Flags: --expose-http2
+'use strict';
+
+require('../common');
+const assert = require('assert');
+
+assert.doesNotThrow(() => require('http2'));
diff --git a/test/parallel/test-http2-write-callbacks.js b/test/parallel/test-http2-write-callbacks.js
new file mode 100644
index 00000000000000..b371ebf681e472
--- /dev/null
+++ b/test/parallel/test-http2-write-callbacks.js
@@ -0,0 +1,36 @@
+// Flags: --expose-http2
+'use strict';
+
+// Verifies that write callbacks are called
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+server.on('stream', common.mustCall((stream) => {
+ stream.write('abc', common.mustCall(() => {
+ stream.end('xyz');
+ }));
+ let actual = '';
+ stream.setEncoding('utf8');
+ stream.on('data', (chunk) => actual += chunk);
+ stream.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz')));
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const req = client.request({ ':method': 'POST' });
+ req.write('abc', common.mustCall(() => {
+ req.end('xyz');
+ }));
+ let actual = '';
+ req.setEncoding('utf8');
+ req.on('data', (chunk) => actual += chunk);
+ req.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz')));
+ req.on('streamClosed', common.mustCall(() => {
+ client.destroy();
+ server.close();
+ }));
+}));
diff --git a/test/parallel/test-http2-write-empty-string.js b/test/parallel/test-http2-write-empty-string.js
new file mode 100644
index 00000000000000..74675fd67d9ef2
--- /dev/null
+++ b/test/parallel/test-http2-write-empty-string.js
@@ -0,0 +1,40 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer(function(request, response) {
+ response.writeHead(200, {'Content-Type': 'text/plain'});
+ response.write('1\n');
+ response.write('');
+ response.write('2\n');
+ response.write('');
+ response.end('3\n');
+
+ this.close();
+});
+
+server.listen(0, common.mustCall(function() {
+ const client = http2.connect(`http://localhost:${this.address().port}`);
+ const headers = { ':path': '/' };
+ const req = client.request(headers).setEncoding('ascii');
+
+ let res = '';
+
+ req.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(200, headers[':status']);
+ }));
+
+ req.on('data', (chunk) => {
+ res += chunk;
+ });
+
+ req.on('end', common.mustCall(function() {
+ assert.strictEqual('1\n2\n3\n', res);
+ client.destroy();
+ }));
+
+ req.end();
+}));
diff --git a/test/parallel/test-http2-zero-length-write.js b/test/parallel/test-http2-zero-length-write.js
new file mode 100644
index 00000000000000..5f4f0681d4baf4
--- /dev/null
+++ b/test/parallel/test-http2-zero-length-write.js
@@ -0,0 +1,50 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http2 = require('http2');
+
+const { Readable } = require('stream');
+
+function getSrc() {
+ const chunks = [ '', 'asdf', '', 'foo', '', 'bar', '' ];
+ return new Readable({
+ read() {
+ const chunk = chunks.shift();
+ if (chunk !== undefined)
+ this.push(chunk);
+ else
+ this.push(null);
+ }
+ });
+}
+
+const expect = 'asdffoobar';
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream) => {
+ let actual = '';
+ stream.respond();
+ stream.resume();
+ stream.setEncoding('utf8');
+ stream.on('data', (chunk) => actual += chunk);
+ stream.on('end', common.mustCall(() => {
+ getSrc().pipe(stream);
+ assert.strictEqual(actual, expect);
+ }));
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ let actual = '';
+ const req = client.request({ ':method': 'POST' });
+ req.on('response', common.mustCall());
+ req.on('data', (chunk) => actual += chunk);
+ req.on('end', common.mustCall(() => {
+ assert.strictEqual(actual, expect);
+ server.close();
+ client.destroy();
+ }));
+ getSrc().pipe(req);
+}));
diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js
index 2a9a676e8d6c2a..e4da5380217e9a 100644
--- a/test/parallel/test-process-versions.js
+++ b/test/parallel/test-process-versions.js
@@ -3,7 +3,7 @@ const common = require('../common');
const assert = require('assert');
const expected_keys = ['ares', 'http_parser', 'modules', 'node',
- 'uv', 'v8', 'zlib'];
+ 'uv', 'v8', 'zlib', 'nghttp2'];
if (common.hasCrypto) {
expected_keys.push('openssl');
diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js
old mode 100755
new mode 100644
|