summaryrefslogtreecommitdiffstats
path: root/test/http2.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--test/http2.js1191
1 files changed, 1191 insertions, 0 deletions
diff --git a/test/http2.js b/test/http2.js
new file mode 100644
index 0000000..71b7749
--- /dev/null
+++ b/test/http2.js
@@ -0,0 +1,1191 @@
+'use strict'
+
+const { createSecureServer } = require('node:http2')
+const { createReadStream, readFileSync } = require('node:fs')
+const { once } = require('node:events')
+const { Blob } = require('node:buffer')
+const { Writable, pipeline, PassThrough, Readable } = require('node:stream')
+
+const { test, plan } = require('tap')
+const { gte } = require('semver')
+const pem = require('https-pem')
+
+const { Client, Agent } = require('..')
+
+const isGreaterThanv20 = gte(process.version.slice(1), '20.0.0')
+// NOTE: node versions <16.14.1 have a bug which changes the order of pseudo-headers
+// https://github.com/nodejs/node/pull/41735
+const hasPseudoHeadersOrderFix = gte(process.version.slice(1), '16.14.1')
+
+plan(23)
+
+test('Should support H2 connection', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers, _flags, rawHeaders) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(6)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test('Should support H2 connection(multiple requests)', async t => {
+ const server = createSecureServer(pem)
+
+ server.on('stream', async (stream, headers, _flags, rawHeaders) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'POST')
+ const reqData = []
+ stream.on('data', chunk => reqData.push(chunk.toString()))
+ await once(stream, 'end')
+ const reqBody = reqData.join('')
+ t.equal(reqBody.length > 0, true)
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end(`hello h2! ${reqBody}`)
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(21)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ for (let i = 0; i < 3; i++) {
+ const sendBody = `seq ${i}`
+ const body = []
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-my-header': 'foo'
+ },
+ body: Readable.from(sendBody)
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), `hello h2! ${sendBody}`)
+ }
+})
+
+test('Should support H2 connection (headers as array)', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers['x-my-drink'], 'coffee,tea')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(7)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: ['x-my-header', 'foo', 'x-my-drink', ['coffee', 'tea']]
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test('Should support H2 connection(POST Buffer)', async t => {
+ const server = createSecureServer({ ...pem, allowHTTP1: false })
+
+ server.on('stream', async (stream, headers, _flags, rawHeaders) => {
+ t.equal(headers[':method'], 'POST')
+ const reqData = []
+ stream.on('data', chunk => reqData.push(chunk.toString()))
+ await once(stream, 'end')
+ t.equal(reqData.join(''), 'hello!')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(6)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const sendBody = 'hello!'
+ const body = []
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ body: sendBody
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test('Should support H2 GOAWAY (server-side)', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.on('session', session => {
+ setTimeout(() => {
+ session.goaway(204)
+ }, 1000)
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(9)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+
+ const [url, disconnectClient, err] = await once(client, 'disconnect')
+
+ t.type(url, URL)
+ t.same(disconnectClient, [client])
+ t.equal(err.message, 'HTTP/2: "GOAWAY" frame received with code 204')
+})
+
+test('Should throw if bad allowH2 has been pased', async t => {
+ try {
+ // eslint-disable-next-line
+ new Client('https://localhost:1000', {
+ allowH2: 'true'
+ })
+ t.fail()
+ } catch (error) {
+ t.equal(error.message, 'allowH2 must be a valid boolean value')
+ }
+})
+
+test('Should throw if bad maxConcurrentStreams has been pased', async t => {
+ try {
+ // eslint-disable-next-line
+ new Client('https://localhost:1000', {
+ allowH2: true,
+ maxConcurrentStreams: {}
+ })
+ t.fail()
+ } catch (error) {
+ t.equal(
+ error.message,
+ 'maxConcurrentStreams must be a possitive integer, greater than 0'
+ )
+ }
+
+ try {
+ // eslint-disable-next-line
+ new Client('https://localhost:1000', {
+ allowH2: true,
+ maxConcurrentStreams: -1
+ })
+ t.fail()
+ } catch (error) {
+ t.equal(
+ error.message,
+ 'maxConcurrentStreams must be a possitive integer, greater than 0'
+ )
+ }
+})
+
+test(
+ 'Request should fail if allowH2 is false and server advertises h1 only',
+ { skip: isGreaterThanv20 },
+ async t => {
+ const server = createSecureServer(
+ {
+ ...pem,
+ allowHTTP1: false,
+ ALPNProtocols: ['http/1.1']
+ },
+ (req, res) => {
+ t.fail('Should not create a valid h2 stream')
+ }
+ )
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ allowH2: false,
+ connect: {
+ rejectUnauthorized: false
+ }
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ t.equal(response.statusCode, 403)
+ }
+)
+
+test(
+ '[v20] Request should fail if allowH2 is false and server advertises h1 only',
+ { skip: !isGreaterThanv20 },
+ async t => {
+ const server = createSecureServer(
+ {
+ ...pem,
+ allowHTTP1: false,
+ ALPNProtocols: ['http/1.1']
+ },
+ (req, res) => {
+ t.fail('Should not create a valid h2 stream')
+ }
+ )
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ allowH2: false,
+ connect: {
+ rejectUnauthorized: false
+ }
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+ t.plan(2)
+
+ try {
+ await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ } catch (error) {
+ t.equal(
+ error.message,
+ 'Client network socket disconnected before secure TLS connection was established'
+ )
+ t.equal(error.code, 'ECONNRESET')
+ }
+ }
+)
+
+test('Should handle h2 continue', async t => {
+ const requestBody = []
+ const server = createSecureServer(pem, () => {})
+ const responseBody = []
+
+ server.on('checkContinue', (request, response) => {
+ t.equal(request.headers.expect, '100-continue')
+ t.equal(request.headers['x-my-header'], 'foo')
+ t.equal(request.headers[':method'], 'POST')
+ response.writeContinue()
+
+ request.on('data', chunk => requestBody.push(chunk))
+
+ response.writeHead(200, {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'foo'
+ })
+ response.end('hello h2!')
+ })
+
+ t.plan(7)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ expectContinue: true,
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ expectContinue: true
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+})
+
+test('Dispatcher#Stream', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ const bufs = []
+ let requestBody = ''
+
+ server.on('stream', async (stream, headers) => {
+ stream.setEncoding('utf-8')
+ stream.on('data', chunk => {
+ requestBody += chunk
+ })
+
+ stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
+ stream.end('hello h2!')
+ })
+
+ t.plan(4)
+
+ server.listen(0, async () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ await client.stream(
+ { path: '/', opaque: { bufs }, method: 'POST', body: expectedBody },
+ ({ statusCode, headers, opaque: { bufs } }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['x-custom'], 'custom-header')
+
+ return new Writable({
+ write (chunk, _encoding, cb) {
+ bufs.push(chunk)
+ cb()
+ }
+ })
+ }
+ )
+
+ t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
+ t.equal(requestBody, expectedBody)
+ })
+})
+
+test('Dispatcher#Pipeline', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ const bufs = []
+ let requestBody = ''
+
+ server.on('stream', async (stream, headers) => {
+ stream.setEncoding('utf-8')
+ stream.on('data', chunk => {
+ requestBody += chunk
+ })
+
+ stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
+ stream.end('hello h2!')
+ })
+
+ t.plan(5)
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ pipeline(
+ new Readable({
+ read () {
+ this.push(Buffer.from(expectedBody))
+ this.push(null)
+ }
+ }),
+ client.pipeline(
+ { path: '/', method: 'POST', body: expectedBody },
+ ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['x-custom'], 'custom-header')
+
+ return pipeline(body, new PassThrough(), () => {})
+ }
+ ),
+ new Writable({
+ write (chunk, _, cb) {
+ bufs.push(chunk)
+ cb()
+ }
+ }),
+ err => {
+ t.error(err)
+ t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
+ t.equal(requestBody, expectedBody)
+ }
+ )
+ })
+})
+
+test('Dispatcher#Connect', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ let requestBody = ''
+
+ server.on('stream', async (stream, headers) => {
+ stream.setEncoding('utf-8')
+ stream.on('data', chunk => {
+ requestBody += chunk
+ })
+
+ stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
+ stream.end('hello h2!')
+ })
+
+ t.plan(6)
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ let result = ''
+ client.connect({ path: '/' }, (err, { socket }) => {
+ t.error(err)
+ socket.on('data', chunk => {
+ result += chunk
+ })
+ socket.on('response', headers => {
+ t.equal(headers[':status'], 200)
+ t.equal(headers['x-custom'], 'custom-header')
+ t.notOk(socket.closed)
+ })
+
+ // We need to handle the error event although
+ // is not controlled by Undici, the fact that a session
+ // is destroyed and destroys subsequent streams, causes
+ // unhandled errors to surface if not handling this event.
+ socket.on('error', () => {})
+
+ socket.once('end', () => {
+ t.equal(requestBody, expectedBody)
+ t.equal(result, 'hello h2!')
+ })
+ socket.end(expectedBody)
+ })
+ })
+})
+
+test('Dispatcher#Upgrade', t => {
+ const server = createSecureServer(pem)
+
+ server.on('stream', async (stream, headers) => {
+ stream.end()
+ })
+
+ t.plan(1)
+
+ server.listen(0, async () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ try {
+ await client.upgrade({ path: '/' })
+ } catch (error) {
+ t.equal(error.message, 'Upgrade not supported for H2')
+ }
+ })
+})
+
+test('Dispatcher#destroy', async t => {
+ const promises = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ setTimeout(stream.end.bind(stream), 1500)
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(4)
+ t.teardown(server.close.bind(server))
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ await client.destroy()
+
+ const results = await Promise.allSettled(promises)
+
+ t.equal(results[0].status, 'rejected')
+ t.equal(results[1].status, 'rejected')
+ t.equal(results[2].status, 'rejected')
+ t.equal(results[3].status, 'rejected')
+})
+
+test('Should handle h2 request with body (string or buffer) - dispatch', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ const response = []
+ const requestBody = []
+
+ server.on('stream', async (stream, headers) => {
+ stream.on('data', chunk => requestBody.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(7)
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ client.dispatch(
+ {
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text/plain'
+ },
+ body: expectedBody
+ },
+ {
+ onConnect () {
+ t.ok(true)
+ },
+ onError (err) {
+ t.error(err)
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(headers['x-custom-h2'], 'foo')
+ },
+ onData (chunk) {
+ response.push(chunk)
+ },
+ onBodySent (body) {
+ t.equal(body.toString('utf-8'), expectedBody)
+ },
+ onComplete () {
+ t.equal(Buffer.concat(response).toString('utf-8'), 'hello h2!')
+ t.equal(
+ Buffer.concat(requestBody).toString('utf-8'),
+ 'hello from client!'
+ )
+ }
+ }
+ )
+ })
+})
+
+test('Should handle h2 request with body (stream)', async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = readFileSync(__filename, 'utf-8')
+ const stream = createReadStream(__filename)
+ const requestChunks = []
+ const responseBody = []
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'PUT')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ for await (const chunk of stream) {
+ requestChunks.push(chunk)
+ }
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body: stream
+ })
+
+ for await (const chunk of response.body) {
+ responseBody.push(chunk)
+ }
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+})
+
+test('Should handle h2 request with body (iterable)', async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello'
+ const requestChunks = []
+ const responseBody = []
+ const iterableBody = {
+ [Symbol.iterator]: function * () {
+ const end = expectedBody.length - 1
+ for (let i = 0; i < end + 1; i++) {
+ yield expectedBody[i]
+ }
+
+ return expectedBody[end]
+ }
+ }
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body: iterableBody
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+})
+
+test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'asd'
+ const requestChunks = []
+ const responseBody = []
+ const body = new Blob(['asd'], {
+ type: 'application/json'
+ })
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+})
+
+test(
+ 'Should handle h2 request with body (Blob:ArrayBuffer)',
+ { skip: !Blob },
+ async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello'
+ const requestChunks = []
+ const responseBody = []
+ const buf = Buffer.from(expectedBody)
+ const body = new ArrayBuffer(buf.byteLength)
+
+ buf.copy(new Uint8Array(body))
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+ }
+)
+
+test('Agent should support H2 connection', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Agent({
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(6)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ origin: `https://localhost:${server.address().port}`,
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test(
+ 'Should provide pseudo-headers in proper order',
+ { skip: !hasPseudoHeadersOrderFix },
+ async t => {
+ const server = createSecureServer(pem)
+ server.on('stream', (stream, _headers, _flags, rawHeaders) => {
+ t.same(rawHeaders, [
+ ':authority',
+ `localhost:${server.address().port}`,
+ ':method',
+ 'GET',
+ ':path',
+ '/',
+ ':scheme',
+ 'https'
+ ])
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ ':status': 200
+ })
+ stream.end()
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ t.equal(response.statusCode, 200)
+ }
+)
+
+test('The h2 pseudo-headers is not included in the headers', async t => {
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ stream.respond({
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(2)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ await response.body.text()
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers[':status'], undefined)
+})