/* globals AbortController */
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after, describe, before } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const EE = require('node:events')
const { kConnect } = require('../lib/core/symbols')
const { Readable } = require('node:stream')
const net = require('node:net')
const { promisify } = require('node:util')
const { NotSupportedError, InvalidArgumentError, AbortError } = require('../lib/core/errors')
const { parseFormDataString } = require('./utils/formdata')
test('request dump head', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-length', 5 * 100)
res.flushHeaders()
res.write('hello'.repeat(100))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
let dumped = false
client.on('disconnect', () => {
t.strictEqual(dumped, true)
})
client.request({
path: '/',
method: 'HEAD'
}, (err, { body }) => {
t.ifError(err)
body.dump({ limit: 1 }).then(() => {
dumped = true
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('request dump big', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-length', 999999999)
while (res.write('asd')) {
// Do nothing...
}
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
let dumped = false
client.on('disconnect', () => {
t.strictEqual(dumped, true)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('data', () => t.fail())
body.dump().then(() => {
dumped = true
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('request dump', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.shouldKeepAlive = false
res.setHeader('content-length', 5)
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
let dumped = false
client.on('disconnect', () => {
t.strictEqual(dumped, true)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.dump().then(() => {
dumped = true
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('request dump with abort signal', async (t) => {
t = tspl(t, { plan: 10 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
const ac = new AbortController()
body.dump({ signal: ac.signal }).catch((err) => {
t.strictEqual(err.name, 'AbortError')
t.strictEqual(err.message, 'This operation was aborted')
const stackLines = err.stack.split('\n').map((l) => l.trim())
t.ok(stackLines[0].startsWith('AbortError: This operation was aborted'))
t.ok(stackLines[1].startsWith('at new DOMException'))
t.ok(stackLines[2].startsWith('at AbortController.abort'))
t.ok(/client-request.js/.test(stackLines[3]))
t.ok(stackLines[4].startsWith('at RequestHandler.runInAsyncScope'))
t.ok(stackLines[5].startsWith('at RequestHandler.onResponseStart'))
t.ok(stackLines[6].startsWith('at Request.onResponseStart'))
})
ac.abort()
})
})
await t.completed
})
test('request dump with POJO as invalid signal', async (t) => {
t = tspl(t, { plan: 9 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.dump({ signal: {} }).catch((err) => {
t.strictEqual(err.name, 'InvalidArgumentError')
t.strictEqual(err.message, 'signal must be an AbortSignal')
const stackLines = err.stack.split('\n').map((l) => l.trim())
t.ok(stackLines[0].startsWith('InvalidArgumentError: signal must be an AbortSignal'))
t.ok(stackLines[1].startsWith('at BodyReadable.dump'))
t.ok(/client-request.js/.test(stackLines[2]))
t.ok(stackLines[3].startsWith('at RequestHandler.runInAsyncScope'))
t.ok(stackLines[4].startsWith('at RequestHandler.onResponseStart'))
t.ok(stackLines[5].startsWith('at Request.onResponseStart'))
})
})
})
await t.completed
})
test('request dump with aborted signal', async (t) => {
t = tspl(t, { plan: 8 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
const ac = new AbortController()
ac.abort(new AbortError('This operation was with purpose aborted'))
body.dump({ signal: ac.signal }).catch((err) => {
t.strictEqual(err.name, 'AbortError')
t.strictEqual(err.message, 'This operation was with purpose aborted')
const stackLines = err.stack.split('\n').map((l) => l.trim())
t.ok(stackLines[0].startsWith('AbortError: This operation was with purpose aborted'))
t.ok(/client-request.js/.test(stackLines[1]))
t.ok(stackLines[2].startsWith('at RequestHandler.runInAsyncScope'))
t.ok(stackLines[3].startsWith('at RequestHandler.onResponseStart'))
t.ok(stackLines[4].startsWith('at Request.onResponseStart'))
})
ac.abort()
})
})
await t.completed
})
test('request hwm', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET',
highWaterMark: 1000
}, (err, { body }) => {
t.ifError(err)
t.deepStrictEqual(body.readableHighWaterMark, 1000)
body.dump()
})
})
await t.completed
})
test('request abort before headers', async (t) => {
t = tspl(t, { plan: 6 })
const signal = new EE()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
signal.emit('abort')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client[kConnect](() => {
client.request({
path: '/',
method: 'GET',
signal
}, (err) => {
t.ok(err instanceof errors.RequestAbortedError)
t.strictEqual(signal.listenerCount('abort'), 0)
})
t.strictEqual(signal.listenerCount('abort'), 1)
client.request({
path: '/',
method: 'GET',
signal
}, (err) => {
t.ok(err instanceof errors.RequestAbortedError)
t.strictEqual(signal.listenerCount('abort'), 0)
})
t.strictEqual(signal.listenerCount('abort'), 2)
})
})
await t.completed
})
test('request body destroyed on invalid callback', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const body = new Readable({
read () { }
})
try {
client.request({
path: '/',
method: 'GET',
body
}, null)
} catch (err) {
t.strictEqual(body.destroyed, true)
}
})
await t.completed
})
test('trailers', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { Trailer: 'Content-MD5' })
res.addTrailers({ 'Content-MD5': 'test' })
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { body, trailers } = await client.request({
path: '/',
method: 'GET'
})
body
.on('data', () => t.fail())
.on('end', () => {
t.deepStrictEqual(trailers, { 'content-md5': 'test' })
})
})
await t.completed
})
test('destroy socket abruptly', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (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))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { statusCode, body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(statusCode, 200)
body.setEncoding('utf8')
let actual = ''
for await (const chunk of body) {
actual += chunk
}
t.strictEqual(actual, 'the body')
})
test('destroy socket abruptly with keep-alive', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (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))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { statusCode, body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(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.ok(true, 'error happened')
}
})
test('request json', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.deepStrictEqual(obj, await body.json())
})
await t.completed
})
test('request long multibyte json', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: 'あ'.repeat(100000) }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.deepStrictEqual(obj, await body.json())
})
await t.completed
})
test('request text', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(JSON.stringify(obj), await body.text())
})
await t.completed
})
describe('headers', () => {
describe('invalid headers', () => {
test('invalid header value - array with string with invalid character', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: ['test\0'] }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
test('invalid header value - array with POJO', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: [{}] }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
test('invalid header value - string with invalid character', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: 'test\0' }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
test('invalid header value - object', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: new Date() }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
})
describe('array', () => {
let serverAddress
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(req.headers))
})
before(async () => {
server.listen(0)
await EE.once(server, 'listening')
serverAddress = `localhost:${server.address().port}`
})
after(() => {
server.closeAllConnections()
server.close()
})
test('empty host header', async (t) => {
t = tspl(t, { plan: 4 })
const client = new Client(`http://${serverAddress}`)
after(() => client.destroy())
const testCase = async (expected, actual) => {
const { body } = await client.request({
path: '/',
method: 'GET',
headers: expected
})
const result = await body.json()
t.deepStrictEqual(result, { ...result, ...actual })
}
await testCase({ key: [null] }, { key: '' })
await testCase({ key: ['test'] }, { key: 'test' })
await testCase({ key: ['test', 'true'] }, { key: 'test, true' })
await testCase({ key: ['test', true] }, { key: 'test, true' })
await t.completed
})
})
describe('host', () => {
let serverAddress
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.headers.host)
})
before(async () => {
server.listen(0)
await EE.once(server, 'listening')
serverAddress = `localhost:${server.address().port}`
})
after(() => {
server.closeAllConnections()
server.close()
})
test('invalid host header', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client(`http://${serverAddress}`)
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: {
host: [
'www.example.com'
]
}
}), new InvalidArgumentError('invalid host header'))
await t.completed
})
test('empty host header', async (t) => {
t = tspl(t, { plan: 3 })
const client = new Client(`http://${serverAddress}`)
after(() => client.destroy())
const getWithHost = async (host, wanted) => {
const { body } = await client.request({
path: '/',
method: 'GET',
headers: { host }
})
t.strictEqual(await body.text(), wanted)
}
await getWithHost('test', 'test')
await getWithHost(undefined, serverAddress)
await getWithHost('', '')
await t.completed
})
})
})
test('request long multibyte text', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: 'あ'.repeat(100000) }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(JSON.stringify(obj), await body.text())
})
await t.completed
})
test('request blob', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const blob = await body.blob()
t.deepStrictEqual(obj, JSON.parse(await blob.text()))
t.strictEqual(blob.type, 'application/json')
})
await t.completed
})
test('request arrayBuffer', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const ab = await body.arrayBuffer()
t.deepStrictEqual(Buffer.from(JSON.stringify(obj)), Buffer.from(ab))
t.ok(ab instanceof ArrayBuffer)
})
await t.completed
})
test('request bytes', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const bytes = await body.bytes()
t.deepStrictEqual(new TextEncoder().encode(JSON.stringify(obj)), bytes)
t.ok(bytes instanceof Uint8Array)
})
await t.completed
})
test('request body', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
let x = ''
for await (const chunk of body.body) {
x += Buffer.from(chunk)
}
t.strictEqual(JSON.stringify(obj), x)
})
await t.completed
})
test('request post body no missing data', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'asd')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET',
body: new Readable({
read () {
this.push('asd')
this.push(null)
}
})
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body no extra data handler', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'asd')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const reqBody = new Readable({
read () {
this.push('asd')
this.push(null)
}
})
process.nextTick(() => {
t.strictEqual(reqBody.listenerCount('data'), 0)
})
const { body } = await client.request({
path: '/',
method: 'GET',
body: reqBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request with onInfo callback', async (t) => {
t = tspl(t, { plan: 3 })
const infos = []
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ foo: 'bar' }))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await client.request({
path: '/',
method: 'GET',
onInfo: (x) => { infos.push(x) }
})
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 102)
t.ok(true, 'pass')
})
await t.completed
})
test('request with onInfo callback but socket is destroyed before end of response', async (t) => {
t = tspl(t, { plan: 5 })
const infos = []
let response
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
response = res
res.writeProcessing()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
try {
await client.request({
path: '/',
method: 'GET',
onInfo: (x) => {
infos.push(x)
response.destroy()
}
})
t.fail()
} catch (e) {
t.ok(e)
t.strictEqual(e.message, 'other side closed')
}
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 102)
t.ok(true, 'pass')
})
await t.completed
})
test('request onInfo callback headers parsing', async (t) => {
t = tspl(t, { plan: 4 })
const infos = []
const server = net.createServer({ joinDuplicateHeaders: true }, (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'))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { body } = await client.request({
path: '/',
method: 'GET',
onInfo: (x) => { infos.push(x) }
})
await body.dump()
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 103)
t.deepStrictEqual(infos[0].headers, { link: '</style.css>; rel=preload; as=style' })
t.ok(true, 'pass')
})
test('request raw responseHeaders', async (t) => {
t = tspl(t, { plan: 4 })
const infos = []
const server = net.createServer({ joinDuplicateHeaders: true }, (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'))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { body, headers } = await client.request({
path: '/',
method: 'GET',
responseHeaders: 'raw',
onInfo: (x) => { infos.push(x) }
})
await body.dump()
t.strictEqual(infos.length, 1)
t.deepStrictEqual(infos[0].headers, ['Link', '</style.css>; rel=preload; as=style'])
t.deepStrictEqual(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close'])
t.ok(true, 'pass')
})
test('request formData', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
try {
await body.formData()
t.fail('should throw NotSupportedError')
} catch (error) {
t.ok(error instanceof NotSupportedError)
}
})
await t.completed
})
test('request text2', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const p = body.text()
let ret = ''
body.on('data', chunk => {
ret += chunk
}).on('end', () => {
t.strictEqual(JSON.stringify(obj), ret)
})
t.strictEqual(JSON.stringify(obj), await p)
})
await t.completed
})
test('request with FormData body', async (t) => {
const { FormData } = require('../')
const fd = new FormData()
fd.set('key', 'value')
fd.set('file', new Blob(['Hello, world!']), 'hello_world.txt')
const server = createServer({ joinDuplicateHeaders: true }, 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.deepStrictEqual(fields[0], { key: 'key', value: 'value' })
t.ok(fileMap.has('file'))
t.strictEqual(fileMap.get('file').data.toString(), 'Hello, world!')
t.deepStrictEqual(fileMap.get('file').info, {
filename: 'hello_world.txt',
encoding: '7bit',
mimeType: 'application/octet-stream'
})
return res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await client.request({
path: '/',
method: 'POST',
body: fd
})
t.end()
})
await t.completed
})
// https://github.com/nodejs/undici/issues/5520
test('request rejects FormData bodies from another implementation', async (t) => {
class FormDataLike {
append () {}
delete () {}
get () {}
getAll () {}
has () {}
set () {}
get [Symbol.toStringTag] () { return 'FormData' }
}
let connections = 0
const client = new Client('http://localhost', {
connect (_, callback) {
connections++
callback(new Error('unexpected connection'))
}
})
t.after(() => client.destroy())
const expectedError = new InvalidArgumentError('body must be a FormData created by this undici package instance; FormData from other sources, such as the built-in FormData provided by Node.js, is not supported')
const bodies = [new globalThis.FormData(), new FormDataLike()]
for (const body of bodies) {
await t.assert.rejects(client.request({
path: '/',
method: 'POST',
body
}), expectedError)
}
t.assert.strictEqual(connections, 0)
})
test('request post body Buffer from string', async (t) => {
t = tspl(t, { plan: 2 })
const requestBody = Buffer.from('abcdefghijklmnopqrstuvwxyz')
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'abcdefghijklmnopqrstuvwxyz')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Buffer from buffer', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = Buffer.from(fullBuffer.buffer, 8, 16)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Uint8Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new Uint8Array(fullBuffer.buffer, 8, 16)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Uint32Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new Uint32Array(fullBuffer.buffer, 8, 4)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Float64Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new Float64Array(fullBuffer.buffer, 8, 2)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body BigUint64Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new BigUint64Array(fullBuffer.buffer, 8, 2)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body DataView', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new DataView(fullBuffer.buffer, 8, 16)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request multibyte json with setEncoding', async (t) => {
t = tspl(t, { plan: 1 })
const asd = Buffer.from('あいうえお')
const data = JSON.stringify({ asd })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(data.slice(0, 1))
setTimeout(() => {
res.write(data.slice(1))
res.end()
}, 100)
})
after(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf8')
t.deepStrictEqual(JSON.parse(data), await body.json())
})
await t.completed
})
test('request multibyte text with setEncoding', async (t) => {
t = tspl(t, { plan: 1 })
const data = Buffer.from('あいうえお')
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(data.slice(0, 1))
setTimeout(() => {
res.write(data.slice(1))
res.end()
}, 100)
})
after(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf8')
t.deepStrictEqual(data.toString('utf8'), await body.text())
})
await t.completed
})
test('request multibyte text with setEncoding', async (t) => {
t = tspl(t, { plan: 1 })
const data = Buffer.from('あいうえお')
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(data.slice(0, 1))
setTimeout(() => {
res.write(data.slice(1))
res.end()
}, 100)
})
after(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('hex')
t.deepStrictEqual(data.toString('hex'), await body.text())
})
await t.completed
})
test('setEncoding(\'utf8\') handles 3-byte UTF-8 characters split across chunks', async (t) => {
t = tspl(t, { plan: 2 })
// CJK character '傳' is 3 bytes: 0xe5, 0x82, 0xb3
// Build a payload where this character will be split at the chunk boundary
const cjkChar = '傳' // U+50B3, bytes: e5 82 b3
const prefix = 'a'.repeat(10) // 10 ASCII bytes
const text = prefix + cjkChar + 'end'
const buf = Buffer.from(text) // 10 + 3 + 3 = 16 bytes
// Split at byte 11, which is in the middle of the 3-byte CJK character
// prefix (10 bytes) + first byte of '傳' (0xe5) | remaining 2 bytes (0x82 0xb3) + 'end'
const chunk1 = buf.subarray(0, 11)
const chunk2 = buf.subarray(11)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
// Send raw buffers to ensure the split is exactly where we want it
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' })
res.write(chunk1)
// Use setTimeout to force separate TCP packets / chunks
setTimeout(() => {
res.end(chunk2)
}, 50)
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf8')
let result = ''
for await (const chunk of body) {
result += chunk
}
// Must not contain U+FFFD replacement characters
t.strictEqual(result.includes('\ufffd'), false, 'should not contain U+FFFD replacement characters')
t.strictEqual(result, text, 'decoded text should match original')
})
await t.completed
})
test('#5611 - setEncoding() then .text() does not truncate a body that arrives in several chunks', async (t) => {
t = tspl(t, { plan: 2 })
// '傳' is 3 bytes in UTF-8, split across the two chunks below.
const text = 'abc傳def'
const buf = Buffer.from(text)
// The rest of the body is released only once the client has called
// setEncoding(), which is what puts the second chunk on the wrong side of it.
const sendRest = new EE()
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' })
res.write(buf.subarray(0, 4))
await EE.once(sendRest, 'go')
res.end(buf.subarray(4))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({ path: '/', method: 'GET' })
body.setEncoding('utf8')
// 'drain' means the request has run to completion, so the whole body has
// arrived and is buffered as decoded strings, with the split character
// held inside the decoder. Waiting on the body itself is not an option:
// reading it would disturb it and make the consume below unusable.
const completed = EE.once(client, 'drain')
sendRest.emit('go')
await completed
const result = await body.text()
t.strictEqual(result, text)
t.strictEqual(Buffer.byteLength(result), buf.length)
})
await t.completed
})
test('#3736 - Aborted Response (without consuming body)', async (t) => {
const plan = tspl(t, { plan: 1 })
const controller = new AbortController()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.writeHead(200, 'ok', {
'content-type': 'text/plain'
})
res.write('hello from server')
res.end()
}, 100)
})
server.listen(0)
await EE.once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(server.close.bind(server))
after(client.destroy.bind(client))
const { signal } = controller
const promise = client.request({
path: '/',
method: 'GET',
signal
})
controller.abort()
await plan.rejects(promise, { message: 'This operation was aborted' })
await plan.completed
})