diff options
Diffstat (limited to 'test/http2.js')
-rw-r--r-- | test/http2.js | 1191 |
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) +}) |