/* globals AbortController */ 'use strict' const { test } = require('tap') const { Client, errors } = require('..') const { createServer } = require('http') const EE = require('events') const { kConnect } = require('../lib/core/symbols') const { Readable } = require('stream') const net = require('net') const { promisify } = require('util') const { NotSupportedError } = require('../lib/core/errors') const { nodeMajor } = require('../lib/core/util') const { parseFormDataString } = require('./utils/formdata') test('request dump', (t) => { t.plan(3) const server = createServer((req, res) => { res.end('hello') }) t.teardown(server.close.bind(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) let dumped = false client.on('disconnect', () => { t.equal(dumped, true) }) client.request({ path: '/', method: 'GET' }, (err, { body }) => { t.error(err) body.dump().then(() => { dumped = true t.pass() }) }) }) }) test('request dump with abort signal', (t) => { t.plan(2) const server = createServer((req, res) => { res.write('hello') }) t.teardown(server.close.bind(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET' }, (err, { body }) => { t.error(err) let ac if (!global.AbortController) { const { AbortController } = require('abort-controller') ac = new AbortController() } else { ac = new AbortController() } body.dump({ signal: ac.signal }).catch((err) => { t.equal(err.name, 'AbortError') server.close() }) ac.abort() }) }) }) test('request hwm', (t) => { t.plan(2) const server = createServer((req, res) => { res.write('hello') }) t.teardown(server.close.bind(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET', highWaterMark: 1000 }, (err, { body }) => { t.error(err) t.same(body.readableHighWaterMark, 1000) body.dump() }) }) }) test('request abort before headers', (t) => { t.plan(6) const signal = new EE() const server = createServer((req, res) => { res.end('hello') signal.emit('abort') }) t.teardown(server.close.bind(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) client[kConnect](() => { client.request({ path: '/', method: 'GET', signal }, (err) => { t.type(err, errors.RequestAbortedError) t.equal(signal.listenerCount('abort'), 0) }) t.equal(signal.listenerCount('abort'), 1) client.request({ path: '/', method: 'GET', signal }, (err) => { t.type(err, errors.RequestAbortedError) t.equal(signal.listenerCount('abort'), 0) }) t.equal(signal.listenerCount('abort'), 2) }) }) }) test('request body destroyed on invalid callback', (t) => { t.plan(1) const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const body = new Readable({ read () {} }) try { client.request({ path: '/', method: 'GET', body }, null) } catch (err) { t.equal(body.destroyed, true) } }) }) test('trailers', (t) => { t.plan(1) const server = createServer((req, res) => { res.writeHead(200, { Trailer: 'Content-MD5' }) res.addTrailers({ 'Content-MD5': 'test' }) res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.close.bind(client)) const { body, trailers } = await client.request({ path: '/', method: 'GET' }) body .on('data', () => t.fail()) .on('end', () => { t.strictSame(trailers, { 'content-md5': 'test' }) }) }) }) test('destroy socket abruptly', { skip: true }, async (t) => { t.plan(2) const server = net.createServer((socket) => { const lines = [ 'HTTP/1.1 200 OK', 'Date: Sat, 09 Oct 2010 14:28:02 GMT', 'Connection: close', '', 'the body' ] socket.end(lines.join('\r\n')) // Unfortunately calling destroy synchronously might get us flaky results, // therefore we delay it to the next event loop run. setImmediate(socket.destroy.bind(socket)) }) t.teardown(server.close.bind(server)) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.close.bind(client)) const { statusCode, body } = await client.request({ path: '/', method: 'GET' }) t.equal(statusCode, 200) body.setEncoding('utf8') let actual = '' for await (const chunk of body) { actual += chunk } t.equal(actual, 'the body') }) test('destroy socket abruptly with keep-alive', { skip: true }, async (t) => { t.plan(2) const server = net.createServer((socket) => { const lines = [ 'HTTP/1.1 200 OK', 'Date: Sat, 09 Oct 2010 14:28:02 GMT', 'Connection: keep-alive', 'Content-Length: 42', '', 'the body' ] socket.end(lines.join('\r\n')) // Unfortunately calling destroy synchronously might get us flaky results, // therefore we delay it to the next event loop run. setImmediate(socket.destroy.bind(socket)) }) t.teardown(server.close.bind(server)) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.close.bind(client)) const { statusCode, body } = await client.request({ path: '/', method: 'GET' }) t.equal(statusCode, 200) body.setEncoding('utf8') try { /* eslint-disable */ for await (const _ of body) { // empty on purpose } /* eslint-enable */ t.fail('no error') } catch (err) { t.pass('error happened') } }) test('request json', (t) => { t.plan(1) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) t.strictSame(obj, await body.json()) }) }) test('request long multibyte json', (t) => { t.plan(1) const obj = { asd: 'あ'.repeat(100000) } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) t.strictSame(obj, await body.json()) }) }) test('request text', (t) => { t.plan(1) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) t.strictSame(JSON.stringify(obj), await body.text()) }) }) test('empty host header', (t) => { t.plan(3) const server = createServer((req, res) => { res.end(req.headers.host) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const serverAddress = `localhost:${server.address().port}` const client = new Client(`http://${serverAddress}`) t.teardown(client.destroy.bind(client)) const getWithHost = async (host, wanted) => { const { body } = await client.request({ path: '/', method: 'GET', headers: { host } }) t.strictSame(await body.text(), wanted) } await getWithHost('test', 'test') await getWithHost(undefined, serverAddress) await getWithHost('', '') }) }) test('request long multibyte text', (t) => { t.plan(1) const obj = { asd: 'あ'.repeat(100000) } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) t.strictSame(JSON.stringify(obj), await body.text()) }) }) test('request blob', { skip: nodeMajor < 16 }, (t) => { t.plan(2) const obj = { asd: true } const server = createServer((req, res) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) const blob = await body.blob() t.strictSame(obj, JSON.parse(await blob.text())) t.equal(blob.type, 'application/json') }) }) test('request arrayBuffer', (t) => { t.plan(2) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) const ab = await body.arrayBuffer() t.strictSame(Buffer.from(JSON.stringify(obj)), Buffer.from(ab)) t.ok(ab instanceof ArrayBuffer) }) }) test('request body', { skip: nodeMajor < 16 }, (t) => { t.plan(1) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) let x = '' for await (const chunk of body.body) { x += Buffer.from(chunk) } t.strictSame(JSON.stringify(obj), x) }) }) test('request post body no missing data', { skip: nodeMajor < 16 }, (t) => { t.plan(2) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'asd') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET', body: new Readable({ read () { this.push('asd') this.push(null) } }), maxRedirections: 2 }) await body.text() t.pass() }) }) test('request post body no extra data handler', { skip: nodeMajor < 16 }, (t) => { t.plan(3) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'asd') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const reqBody = new Readable({ read () { this.push('asd') this.push(null) } }) process.nextTick(() => { t.equal(reqBody.listenerCount('data'), 0) }) const { body } = await client.request({ path: '/', method: 'GET', body: reqBody, maxRedirections: 0 }) await body.text() t.pass() }) }) test('request with onInfo callback', (t) => { t.plan(3) const infos = [] const server = createServer((req, res) => { res.writeProcessing() res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ foo: 'bar' })) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) await client.request({ path: '/', method: 'GET', onInfo: (x) => { infos.push(x) } }) t.equal(infos.length, 1) t.equal(infos[0].statusCode, 102) t.pass() }) }) test('request with onInfo callback but socket is destroyed before end of response', (t) => { t.plan(5) const infos = [] let response const server = createServer((req, res) => { response = res res.writeProcessing() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) try { await client.request({ path: '/', method: 'GET', onInfo: (x) => { infos.push(x) response.destroy() } }) t.error() } catch (e) { t.ok(e) t.equal(e.message, 'other side closed') } t.equal(infos.length, 1) t.equal(infos[0].statusCode, 102) t.pass() }) }) test('request onInfo callback headers parsing', async (t) => { t.plan(4) const infos = [] const server = net.createServer((socket) => { const lines = [ 'HTTP/1.1 103 Early Hints', 'Link: ; rel=preload; as=style', '', 'HTTP/1.1 200 OK', 'Date: Sat, 09 Oct 2010 14:28:02 GMT', 'Connection: close', '', 'the body' ] socket.end(lines.join('\r\n')) }) t.teardown(server.close.bind(server)) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.close.bind(client)) const { body } = await client.request({ path: '/', method: 'GET', onInfo: (x) => { infos.push(x) } }) await body.dump() t.equal(infos.length, 1) t.equal(infos[0].statusCode, 103) t.same(infos[0].headers, { link: '; rel=preload; as=style' }) t.pass() }) test('request raw responseHeaders', async (t) => { t.plan(4) const infos = [] const server = net.createServer((socket) => { const lines = [ 'HTTP/1.1 103 Early Hints', 'Link: ; rel=preload; as=style', '', 'HTTP/1.1 200 OK', 'Date: Sat, 09 Oct 2010 14:28:02 GMT', 'Connection: close', '', 'the body' ] socket.end(lines.join('\r\n')) }) t.teardown(server.close.bind(server)) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.close.bind(client)) const { body, headers } = await client.request({ path: '/', method: 'GET', responseHeaders: 'raw', onInfo: (x) => { infos.push(x) } }) await body.dump() t.equal(infos.length, 1) t.same(infos[0].headers, ['Link', '; rel=preload; as=style']) t.same(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close']) t.pass() }) test('request formData', { skip: nodeMajor < 16 }, (t) => { t.plan(1) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) try { await body.formData() t.fail('should throw NotSupportedError') } catch (error) { t.ok(error instanceof NotSupportedError) } }) }) test('request text2', (t) => { t.plan(2) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'GET' }) const p = body.text() let ret = '' body.on('data', chunk => { ret += chunk }).on('end', () => { t.equal(JSON.stringify(obj), ret) }) t.strictSame(JSON.stringify(obj), await p) }) }) test('request with FormData body', { skip: nodeMajor < 16 }, (t) => { const { FormData } = require('../') const { Blob } = require('buffer') const fd = new FormData() fd.set('key', 'value') fd.set('file', new Blob(['Hello, world!']), 'hello_world.txt') const server = createServer(async (req, res) => { const contentType = req.headers['content-type'] // ensure we received a multipart/form-data header t.ok(/^multipart\/form-data; boundary=-+formdata-undici-0\d+$/.test(contentType)) const chunks = [] for await (const chunk of req) { chunks.push(chunk) } const { fileMap, fields } = await parseFormDataString( Buffer.concat(chunks), contentType ) t.same(fields[0], { key: 'key', value: 'value' }) t.ok(fileMap.has('file')) t.equal(fileMap.get('file').data.toString(), 'Hello, world!') t.same(fileMap.get('file').info, { filename: 'hello_world.txt', encoding: '7bit', mimeType: 'application/octet-stream' }) return res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) await client.request({ path: '/', method: 'POST', body: fd }) t.end() }) }) test('request with FormData body on node < 16', { skip: nodeMajor >= 16 }, async (t) => { t.plan(1) // a FormData polyfill, for example class FormData {} const fd = new FormData() const client = new Client('http://localhost:3000') t.teardown(client.destroy.bind(client)) await t.rejects(client.request({ path: '/', method: 'POST', body: fd }), errors.InvalidArgumentError) }) test('request post body Buffer from string', (t) => { t.plan(2) const requestBody = Buffer.from('abcdefghijklmnopqrstuvwxyz') const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'abcdefghijklmnopqrstuvwxyz') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'POST', body: requestBody, maxRedirections: 2 }) await body.text() t.pass() }) }) test('request post body Buffer from buffer', (t) => { t.plan(2) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = Buffer.from(fullBuffer.buffer, 8, 16) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'ijklmnopqrstuvwx') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'POST', body: requestBody, maxRedirections: 2 }) await body.text() t.pass() }) }) test('request post body Uint8Array', (t) => { t.plan(2) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new Uint8Array(fullBuffer.buffer, 8, 16) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'ijklmnopqrstuvwx') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'POST', body: requestBody, maxRedirections: 2 }) await body.text() t.pass() }) }) test('request post body Uint32Array', (t) => { t.plan(2) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new Uint32Array(fullBuffer.buffer, 8, 4) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'ijklmnopqrstuvwx') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'POST', body: requestBody, maxRedirections: 2 }) await body.text() t.pass() }) }) test('request post body Float64Array', (t) => { t.plan(2) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new Float64Array(fullBuffer.buffer, 8, 2) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'ijklmnopqrstuvwx') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'POST', body: requestBody, maxRedirections: 2 }) await body.text() t.pass() }) }) test('request post body BigUint64Array', (t) => { t.plan(2) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new BigUint64Array(fullBuffer.buffer, 8, 2) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'ijklmnopqrstuvwx') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'POST', body: requestBody, maxRedirections: 2 }) await body.text() t.pass() }) }) test('request post body DataView', (t) => { t.plan(2) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new DataView(fullBuffer.buffer, 8, 16) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } t.equal(ret, 'ijklmnopqrstuvwx') res.end() }) t.teardown(server.close.bind(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.destroy.bind(client)) const { body } = await client.request({ path: '/', method: 'POST', body: requestBody, maxRedirections: 2 }) await body.text() t.pass() }) })