diff options
Diffstat (limited to 'test/client-request.js')
-rw-r--r-- | test/client-request.js | 997 |
1 files changed, 997 insertions, 0 deletions
diff --git a/test/client-request.js b/test/client-request.js new file mode 100644 index 0000000..3e66705 --- /dev/null +++ b/test/client-request.js @@ -0,0 +1,997 @@ +/* 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: </style.css>; 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: '</style.css>; 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: </style.css>; 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', '</style.css>; 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() + }) +}) |