Socket
Socket
Sign inDemoInstall

undici

Package Overview
Dependencies
Maintainers
2
Versions
211
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

undici - npm Package Compare versions

Comparing version 3.3.4 to 4.0.0-alpha.0

docs/api/Agent.md

73

index.js
'use strict'
const Client = require('./lib/core/client')
const Client = require('./lib/client')
const Dispatcher = require('./lib/dispatcher')
const errors = require('./lib/core/errors')
const Pool = require('./lib/pool')
const { Agent, request, stream, pipeline, setGlobalAgent } = require('./lib/agent')
const Agent = require('./lib/agent')
const util = require('./lib/core/util')
const { InvalidArgumentError } = require('./lib/core/errors')
const api = require('./lib/api')
const MockClient = require('./lib/mock/mock-client')
const MockAgent = require('./lib/mock/mock-agent')
const MockPool = require('./lib/mock/mock-pool')
Client.prototype.request = require('./lib/client-request')
Client.prototype.stream = require('./lib/client-stream')
Client.prototype.pipeline = require('./lib/client-pipeline')
Client.prototype.upgrade = require('./lib/client-upgrade')
Client.prototype.connect = require('./lib/client-connect')
Object.assign(Dispatcher.prototype, api)
function undici (url, opts) {
return new Pool(url, opts)
module.exports.Dispatcher = Dispatcher
module.exports.Client = Client
module.exports.Pool = Pool
module.exports.Agent = Agent
module.exports.errors = errors
let globalDispatcher = new Agent()
function setGlobalDispatcher (agent) {
if (!agent || typeof agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument agent must implement Agent')
}
globalDispatcher = agent
}
undici.Pool = Pool
undici.Client = Client
undici.errors = errors
function getGlobalDispatcher () {
return globalDispatcher
}
undici.Agent = Agent
undici.request = request
undici.stream = stream
undici.pipeline = pipeline
undici.setGlobalAgent = setGlobalAgent
function makeDispatcher (fn) {
return (url, { agent, dispatcher = getGlobalDispatcher(), method = 'GET', ...opts } = {}, ...additionalArgs) => {
if (opts.path != null) {
throw new InvalidArgumentError('unsupported opts.path')
}
module.exports = undici
if (agent) {
throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?')
}
const { origin, pathname, search } = util.parseURL(url)
const path = search ? `${pathname}${search}` : pathname
return fn.call(dispatcher, { ...opts, origin, method, path }, ...additionalArgs)
}
}
module.exports.setGlobalDispatcher = setGlobalDispatcher
module.exports.getGlobalDispatcher = getGlobalDispatcher
module.exports.request = makeDispatcher(api.request)
module.exports.stream = makeDispatcher(api.stream)
module.exports.pipeline = makeDispatcher(api.pipeline)
module.exports.connect = makeDispatcher(api.connect)
module.exports.upgrade = makeDispatcher(api.upgrade)
module.exports.MockClient = MockClient
module.exports.MockPool = MockPool
module.exports.MockAgent = MockAgent
'use strict'
const { InvalidArgumentError, InvalidReturnValueError } = require('./core/errors')
const {
ClientClosedError,
InvalidArgumentError,
ClientDestroyedError
} = require('./core/errors')
const { kClients, kPending, kRunning, kSize, kConnected } = require('./core/symbols')
const Dispatcher = require('./dispatcher')
const Pool = require('./pool')
const Client = require('./client')
const util = require('./core/util')
const { kAgentOpts, kAgentCache } = require('./core/symbols')
const assert = require('assert')
const RedirectHandler = require('./handler/redirect')
const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()
class Agent {
constructor (opts) {
this[kAgentOpts] = opts
this[kAgentCache] = new Map()
}
const kDestroyed = Symbol('destroyed')
const kClosed = Symbol('closed')
const kOnConnect = Symbol('onConnect')
const kOnDisconnect = Symbol('onDisconnect')
const kMaxRedirections = Symbol('maxRedirections')
const kOnDrain = Symbol('onDrain')
const kFactory = Symbol('factory')
const kFinalizer = Symbol('finalizer')
const kOptions = Symbol('options')
get (origin) {
if (typeof origin !== 'string' || origin === '') {
throw new InvalidArgumentError('Origin must be a non-empty string.')
function defaultFactory (origin, opts) {
return opts && opts.connections === 1
? new Client(origin, opts)
: new Pool(origin, opts)
}
class Agent extends Dispatcher {
constructor ({ factory = defaultFactory, maxRedirections = 0, ...options } = {}) {
super()
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
const self = this
let pool = self[kAgentCache].get(origin)
if (!Number.isInteger(maxRedirections) || maxRedirections < 0) {
throw new InvalidArgumentError('maxRedirections must be a positive number')
}
function onDisconnect () {
if (this.connected === 0 && this.size === 0) {
this.off('disconnect', onDisconnect)
self[kAgentCache].delete(origin)
this[kOptions] = JSON.parse(JSON.stringify(options))
this[kMaxRedirections] = maxRedirections
this[kFactory] = factory
this[kClients] = new Map()
this[kFinalizer] = new FinalizationRegistry(key => /* istanbul ignore next: gc is undeterministic */{
const ref = this[kClients].get(key)
if (ref !== undefined && ref.deref() === undefined) {
this[kClients].delete(key)
}
})
this[kClosed] = false
this[kDestroyed] = false
const agent = this
this[kOnDrain] = (origin, targets) => {
agent.emit('drain', origin, [agent, ...targets])
}
if (!pool) {
pool = new Pool(origin, self[kAgentOpts])
pool.on('disconnect', onDisconnect)
self[kAgentCache].set(origin, pool)
this[kOnConnect] = (origin, targets) => {
agent.emit('connect', origin, [agent, ...targets])
}
return pool
this[kOnDisconnect] = (origin, targets, err) => {
agent.emit('disconnect', origin, [agent, ...targets], err)
}
}
close () {
const closePromises = []
for (const pool of this[kAgentCache].values()) {
closePromises.push(pool.close())
/* istanbul ignore next: only used for test */
get [kConnected] () {
let ret = 0
for (const ref of this[kClients].values()) {
const client = ref.deref()
if (client) {
ret += client[kConnected]
}
}
return Promise.all(closePromises)
return ret
}
destroy () {
const destroyPromises = []
for (const pool of this[kAgentCache].values()) {
destroyPromises.push(pool.destroy())
/* istanbul ignore next: only used for test */
get [kPending] () {
let ret = 0
for (const ref of this[kClients].values()) {
const client = ref.deref()
if (client) {
ret += client[kPending]
}
}
return Promise.all(destroyPromises)
return ret
}
}
let globalAgent = new Agent({ connections: null })
/* istanbul ignore next: only used for test */
get [kRunning] () {
let ret = 0
for (const ref of this[kClients].values()) {
const client = ref.deref()
if (client) {
ret += client[kRunning]
}
}
return ret
}
function setGlobalAgent (agent) {
if (!agent || typeof agent.get !== 'function') {
throw new InvalidArgumentError('Argument agent must implement Agent')
/* istanbul ignore next: only used for test */
get [kSize] () {
let ret = 0
for (const ref of this[kClients].values()) {
const client = ref.deref()
if (client) {
ret += client[kSize]
}
}
return ret
}
globalAgent = agent
}
function dispatchFromAgent (requestType) {
return (url, { agent = globalAgent, method = 'GET', ...opts } = {}, ...additionalArgs) => {
if (opts.path != null) {
throw new InvalidArgumentError('unsupported opts.path')
dispatch (opts, handler) {
if (!handler || typeof handler !== 'object') {
throw new InvalidArgumentError('handler')
}
const { origin, pathname, search } = util.parseURL(url)
try {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('opts must be a object.')
}
const path = `${pathname || '/'}${search || ''}`
if (typeof opts.origin !== 'string' || opts.origin === '') {
throw new InvalidArgumentError('opts.origin must be a non-empty string.')
}
const client = agent.get(origin)
if (this[kDestroyed]) {
throw new ClientDestroyedError()
}
if (client && typeof client[requestType] !== 'function') {
throw new InvalidReturnValueError(`Client returned from Agent.get() does not implement method ${requestType}`)
if (this[kClosed]) {
throw new ClientClosedError()
}
const ref = this[kClients].get(opts.origin)
let dispatcher = ref ? ref.deref() : null
if (!dispatcher) {
dispatcher = this[kFactory](opts.origin, this[kOptions])
.on('connect', this[kOnConnect])
.on('disconnect', this[kOnDisconnect])
.on('drain', this[kOnDrain])
this[kClients].set(opts.origin, new WeakRef(dispatcher))
this[kFinalizer].register(dispatcher, opts.origin)
}
const { maxRedirections = this[kMaxRedirections] } = opts
if (!Number.isInteger(maxRedirections) || maxRedirections < 0) {
throw new InvalidArgumentError('maxRedirections must be a positive number')
}
if (!maxRedirections) {
return dispatcher.dispatch(opts, handler)
}
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
// so that it can be dispatched again?
// TODO (fix): Do we need 100-expect support to provide a way to do this properly?
return dispatcher.dispatch(opts, handler)
}
/* istanbul ignore next */
if (util.isStream(opts.body)) {
opts.body
.on('data', function () {
assert(false)
})
}
return dispatcher.dispatch(opts, new RedirectHandler(this, opts, handler))
} catch (err) {
if (typeof handler.onError !== 'function') {
throw new InvalidArgumentError('invalid onError method')
}
handler.onError(err)
}
}
return client[requestType]({ ...opts, method, path }, ...additionalArgs)
get closed () {
return this[kClosed]
}
get destroyed () {
return this[kDestroyed]
}
close (callback) {
if (callback != null && typeof callback !== 'function') {
throw new InvalidArgumentError('callback must be a function')
}
this[kClosed] = true
const closePromises = []
for (const ref of this[kClients].values()) {
const client = ref.deref()
/* istanbul ignore else: gc is undeterministic */
if (client) {
closePromises.push(client.close())
}
}
if (!callback) {
return Promise.all(closePromises)
}
// Should never error.
Promise.all(closePromises).then(() => process.nextTick(callback))
}
destroy (err, callback) {
if (typeof err === 'function') {
callback = err
err = null
}
if (callback != null && typeof callback !== 'function') {
throw new InvalidArgumentError('callback must be a function')
}
this[kClosed] = true
this[kDestroyed] = true
const destroyPromises = []
for (const ref of this[kClients].values()) {
const client = ref.deref()
/* istanbul ignore else: gc is undeterministic */
if (client) {
destroyPromises.push(client.destroy(err))
}
}
if (!callback) {
return Promise.all(destroyPromises)
}
// Should never error.
Promise.all(destroyPromises).then(() => process.nextTick(callback))
}
}
module.exports = {
request: dispatchFromAgent('request'),
stream: dispatchFromAgent('stream'),
pipeline: dispatchFromAgent('pipeline'),
connect: dispatchFromAgent('connect'),
upgrade: dispatchFromAgent('upgrade'),
setGlobalAgent,
Agent
}
module.exports = Agent

@@ -11,2 +11,12 @@ 'use strict'

class ConnectTimeoutError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, ConnectTimeoutError)
this.name = 'ConnectTimeoutError'
this.message = message || 'Connect Timeout Error'
this.code = 'UND_ERR_CONNECT_TIMEOUT'
}
}
class HeadersTimeoutError extends UndiciError {

@@ -22,2 +32,12 @@ constructor (message) {

class HeadersOverflowError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, HeadersOverflowError)
this.name = 'HeadersOverflowError'
this.message = message || 'Headers Overflow Error'
this.code = 'UND_ERR_HEADERS_OVERFLOW'
}
}
class BodyTimeoutError extends UndiciError {

@@ -38,3 +58,3 @@ constructor (message) {

this.name = 'InvalidArgumentError'
this.message = message || /* istanbul ignore next */ 'Invalid Argument Error'
this.message = message || 'Invalid Argument Error'
this.code = 'UND_ERR_INVALID_ARG'

@@ -49,3 +69,3 @@ }

this.name = 'InvalidReturnValueError'
this.message = message || /* istanbul ignore next */ 'Invalid Return Value Error'
this.message = message || 'Invalid Return Value Error'
this.code = 'UND_ERR_INVALID_RETURN_VALUE'

@@ -60,3 +80,3 @@ }

this.name = 'RequestAbortedError'
this.message = message || /* istanbul ignore next */ 'Request aborted'
this.message = message || 'Request aborted'
this.code = 'UND_ERR_ABORTED'

@@ -71,3 +91,3 @@ }

this.name = 'InformationalError'
this.message = message || /* istanbul ignore next */ 'Request information'
this.message = message || 'Request information'
this.code = 'UND_ERR_INFO'

@@ -82,3 +102,3 @@ }

this.name = 'ContentLengthMismatchError'
this.message = message || /* istanbul ignore next */ 'Request body length does not match content-length header'
this.message = message || 'Request body length does not match content-length header'
this.code = 'UND_ERR_CONTENT_LENGTH_MISMATCH'

@@ -93,3 +113,3 @@ }

this.name = 'TrailerMismatchError'
this.message = message || /* istanbul ignore next */ 'Trailers does not match trailer header'
this.message = message || 'Trailers does not match trailer header'
this.code = 'UND_ERR_TRAILER_MISMATCH'

@@ -104,3 +124,3 @@ }

this.name = 'ClientDestroyedError'
this.message = message || /* istanbul ignore next */ 'The client is destroyed'
this.message = message || 'The client is destroyed'
this.code = 'UND_ERR_DESTROYED'

@@ -115,3 +135,3 @@ }

this.name = 'ClientClosedError'
this.message = message || /* istanbul ignore next */ 'The client is closed'
this.message = message || 'The client is closed'
this.code = 'UND_ERR_CLOSED'

@@ -126,3 +146,3 @@ }

this.name = 'SocketError'
this.message = message || /* istanbul ignore next */ 'Socket error'
this.message = message || 'Socket error'
this.code = 'UND_ERR_SOCKET'

@@ -137,3 +157,3 @@ }

this.name = 'NotSupportedError'
this.message = message || /* istanbul ignore next */ 'Not supported error'
this.message = message || 'Not supported error'
this.code = 'UND_ERR_NOT_SUPPORTED'

@@ -146,4 +166,6 @@ }

HeadersTimeoutError,
HeadersOverflowError,
BodyTimeoutError,
ContentLengthMismatchError,
ConnectTimeoutError,
TrailerMismatchError,

@@ -150,0 +172,0 @@ InvalidArgumentError,

@@ -12,6 +12,2 @@ 'use strict'

// Borrowed from https://gist.github.com/dperini/729294
// eslint-disable-next-line no-control-regex
const REGEXP_ABSOLUTE_URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x00a1-\xffff0-9]+-?)*[a-z\x00a1-\xffff0-9]+)(?:\.(?:[a-z\x00a1-\xffff0-9]+-?)*[a-z\x00a1-\xffff0-9]+)*(?:\.(?:[a-z\x00a1-\xffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/ius
class Request {

@@ -24,7 +20,9 @@ constructor ({

idempotent,
upgrade
upgrade,
headersTimeout,
bodyTimeout
}, handler) {
if (typeof path !== 'string') {
throw new InvalidArgumentError('path must be a string')
} else if (path[0] !== '/' && !REGEXP_ABSOLUTE_URL.test(path)) {
} else if (path[0] !== '/' && !(path.startsWith('http://') || path.startsWith('https://'))) {
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')

@@ -41,2 +39,14 @@ }

if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
throw new InvalidArgumentError('invalid headersTimeout')
}
if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
throw new InvalidArgumentError('invalid bodyTimeout')
}
this.headersTimeout = headersTimeout
this.bodyTimeout = bodyTimeout
this.method = method

@@ -46,3 +56,3 @@

this.body = null
} else if (util.isStream(body)) {
} else if (util.isReadable(body)) {
this.body = body

@@ -57,5 +67,7 @@ } else if (util.isBuffer(body)) {

this.completed = false
this.aborted = false
this.upgrade = upgrade || method === 'CONNECT' || null
this.upgrade = upgrade || null

@@ -97,3 +109,3 @@ this.path = path

if (this.upgrade) {
if (this.upgrade || this.method === 'CONNECT') {
if (typeof handler.onUpgrade !== 'function') {

@@ -121,2 +133,4 @@ throw new InvalidArgumentError('invalid onUpgrade method')

assert(!this.aborted)
assert(!this.completed)
return this[kHandler].onConnect(abort)

@@ -127,2 +141,4 @@ }

assert(!this.aborted)
assert(!this.completed)
return this[kHandler].onHeaders(statusCode, headers, resume)

@@ -133,3 +149,4 @@ }

assert(!this.aborted)
assert(!this.upgrade)
assert(!this.completed)
return this[kHandler].onData(chunk)

@@ -140,3 +157,4 @@ }

assert(!this.aborted)
assert(this.upgrade)
assert(!this.completed)
return this[kHandler].onUpgrade(statusCode, headers, socket)

@@ -147,3 +165,4 @@ }

assert(!this.aborted)
assert(!this.upgrade)
this.completed = true
return this[kHandler].onComplete(trailers)

@@ -157,5 +176,3 @@ }

this.aborted = true
// Ensure all queued handlers are invoked before calling onError.
util.queueMicrotask(() => this[kHandler].onError(err))
return this[kHandler].onError(err)
}

@@ -162,0 +179,0 @@ }

@@ -7,2 +7,4 @@ module.exports = {

kConnect: Symbol('connect'),
kConnectTimeout: Symbol('connect timeout'),
kConnectTimeoutValue: Symbol('connect timeou valuet'),
kIdleTimeout: Symbol('idle timeout'),

@@ -19,2 +21,7 @@ kIdleTimeoutValue: Symbol('idle timeout value'),

kHost: Symbol('host'),
kRunning: Symbol('running'),
kPending: Symbol('pending'),
kSize: Symbol('size'),
kBusy: Symbol('busy'),
kConnected: Symbol('connected'),
kTLSOpts: Symbol('TLS Options'),

@@ -29,2 +36,3 @@ kClosed: Symbol('closed'),

kError: Symbol('error'),
kClients: Symbol('clients'),
kClient: Symbol('client'),

@@ -37,5 +45,5 @@ kParser: Symbol('parser'),

kTLSSession: Symbol('tls session cache'),
kSetTLSSession: Symbol('set tls session'),
kHostHeader: Symbol('host header'),
kAgentOpts: Symbol('agent opts'),
kAgentCache: Symbol('agent cache')
kStrictContentLength: Symbol('strict content length')
}

@@ -6,3 +6,2 @@ 'use strict'

const { IncomingMessage } = require('http')
const util = require('util')
const net = require('net')

@@ -13,6 +12,16 @@ const { InvalidArgumentError } = require('./errors')

function isStream (body) {
return !!(body && typeof body.on === 'function')
function isReadable (obj) {
return !!(obj && typeof obj.pipe === 'function' &&
typeof obj.on === 'function')
}
function isWritable (obj) {
return !!(obj && typeof obj.write === 'function' &&
typeof obj.on === 'function')
}
function isStream (obj) {
return isReadable(obj) || isWritable(obj)
}
function parseURL (url) {

@@ -71,3 +80,5 @@ if (typeof url === 'string') {

const idx = servername.indexOf(']')
servername = idx === -1 ? servername : servername.substr(1, idx - 1)
assert(idx !== -1)
servername = servername.substr(1, idx - 1)
} else {

@@ -129,6 +140,6 @@ servername = servername.split(':', 1)[0]

for (let i = 0; i < headers.length; i += 2) {
const key = headers[i].toLowerCase()
const key = headers[i].toString().toLowerCase()
let val = obj[key]
if (!val) {
obj[key] = headers[i + 1]
obj[key] = headers[i + 1].toString()
} else {

@@ -139,3 +150,3 @@ if (!Array.isArray(val)) {

}
val.push(headers[i + 1])
val.push(headers[i + 1].toString())
}

@@ -151,13 +162,2 @@ }

function errnoException (code, syscall) {
const name = util.getSystemErrorName(code)
const err = new Error(`${syscall} ${name}`)
err.errno = err
err.code = code
err.syscall = syscall
return err
}
module.exports = {

@@ -168,4 +168,4 @@ nop,

getServerName,
errnoException,
isStream,
isReadable,
isDestroyed,

@@ -176,6 +176,3 @@ parseHeaders,

bodyLength,
isBuffer,
queueMicrotask: global.queueMicrotask || (cb => Promise.resolve()
.then(cb)
.catch(err => setTimeout(() => { throw err }, 0)))
isBuffer
}
'use strict'
const EventEmitter = require('events')
const Client = require('./core/client')
const Dispatcher = require('./dispatcher')
const Client = require('./client')
const {

@@ -12,2 +12,4 @@ ClientClosedError,

const util = require('./core/util')
const { kTLSSession, kSize, kConnected, kRunning, kPending, kUrl, kBusy } = require('./core/symbols')
const assert = require('assert')

@@ -21,12 +23,16 @@ const kClients = Symbol('clients')

const kOptions = Symbol('options')
const kUrl = Symbol('url')
const kOnDrain = Symbol('onDrain')
const kOnConnect = Symbol('onConnect')
const kOnDisconnect = Symbol('onDisconnect')
const kPending = Symbol('pending')
const kConnected = Symbol('connected')
const kOnTLSSession = Symbol('onTLSSession')
const kConnections = Symbol('connections')
const kFactory = Symbol('factory')
const kQueued = Symbol('queued')
class Pool extends EventEmitter {
constructor (origin, { connections, ...options } = {}) {
function defaultFactory (origin, opts) {
return new Client(origin, opts)
}
class Pool extends Dispatcher {
constructor (origin, { connections, factory = defaultFactory, ...options } = {}) {
super()

@@ -38,2 +44,6 @@

if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
this[kConnections] = connections || null

@@ -48,11 +58,15 @@ this[kUrl] = util.parseOrigin(origin)

this[kNeedDrain] = false
this[kPending] = 0
this[kConnected] = 0
this[kQueued] = 0
this[kFactory] = factory
const pool = this
this[kOnDrain] = function onDrain () {
this[kOnDrain] = function onDrain (url, targets) {
assert(pool[kUrl].origin === url.origin)
const queue = pool[kQueue]
while (!this.busy) {
let needDrain = false
while (!needDrain) {
const item = queue.shift()

@@ -62,9 +76,11 @@ if (!item) {

}
pool[kPending]--
this.dispatch(item.opts, item.handler)
pool[kQueued]--
needDrain = !this.dispatch(item.opts, item.handler)
}
if (pool[kNeedDrain] && !this.busy) {
this[kNeedDrain] = needDrain
if (!this[kNeedDrain] && pool[kNeedDrain]) {
pool[kNeedDrain] = false
pool.emit('drain')
pool.emit('drain', origin, [pool, ...targets])
}

@@ -79,65 +95,55 @@

this[kOnConnect] = function onConnect () {
pool[kConnected]++
pool.emit('connect', this)
this[kOnConnect] = (origin, targets) => {
pool.emit('connect', origin, [pool, ...targets])
}
this[kOnDisconnect] = function onDisconnect (err) {
pool[kConnected]--
pool.emit('disconnect', this, err)
this[kOnDisconnect] = (origin, targets, err) => {
pool.emit('disconnect', origin, [pool, ...targets], err)
}
}
get url () {
return this[kUrl]
}
get connected () {
return this[kConnected]
}
get busy () {
if (this[kPending] > 0) {
return true
}
if (this[kConnections] && this[kClients].length === this[kConnections]) {
for (const { busy } of this[kClients]) {
if (!busy) {
return false
}
this[kOnTLSSession] = function cacheClientTLSSession (session) {
if (session) {
pool[kTLSSession] = session
}
return true
}
}
return false
/* istanbul ignore next: only used for test */
get [kBusy] () {
return this[kNeedDrain]
}
get pending () {
let ret = this[kPending]
for (const { pending } of this[kClients]) {
ret += pending
/* istanbul ignore next: only used for test */
get [kConnected] () {
let ret = 0
for (const { [kConnected]: connected } of this[kClients]) {
ret += connected
}
return ret
}
get running () {
/* istanbul ignore next: only used for test */
get [kRunning] () {
let ret = 0
for (const { running } of this[kClients]) {
for (const { [kRunning]: running } of this[kClients]) {
ret += running
}
return ret
}
/* istanbul ignore next: only used for test */
get [kPending] () {
let ret = this[kQueued]
for (const { [kPending]: pending } of this[kClients]) {
ret += pending
}
return ret
}
get size () {
let ret = this[kPending]
for (const { size } of this[kClients]) {
/* istanbul ignore: only used for test */
get [kSize] () {
let ret = this[kQueued]
for (const { [kSize]: size } of this[kClients]) {
ret += size
}
return ret

@@ -155,3 +161,11 @@ }

dispatch (opts, handler) {
if (!handler || typeof handler !== 'object') {
throw new InvalidArgumentError('handler')
}
try {
if (opts.origin && opts.origin !== this[kUrl].origin) {
throw new InvalidArgumentError('origin')
}
if (this[kDestroyed]) {

@@ -165,7 +179,18 @@ throw new ClientDestroyedError()

let client = this[kClients].find(client => !client.busy)
let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain])
if (!client) {
if (!dispatcher) {
if (!this[kConnections] || this[kClients].length < this[kConnections]) {
client = new Client(this[kUrl], this[kOptions])
let options = this[kOptions]
if (
options.tls &&
options.tls.reuseSessions !== false &&
!options.tls.session &&
this[kTLSSession]
) {
options = { ...options, tls: { ...options.tls, session: this[kTLSSession] } }
}
dispatcher = this[kFactory](this[kUrl], options)
.on('drain', this[kOnDrain])

@@ -175,15 +200,17 @@ .on('connect', this[kOnConnect])

this[kClients].push(client)
if (!options.tls || (options.tls.reuseSessions !== false && !options.tls.session)) {
dispatcher.on('session', this[kOnTLSSession])
}
this[kClients].push(dispatcher)
}
}
if (!client) {
if (!dispatcher) {
this[kNeedDrain] = true
this[kQueue].push({ opts, handler })
this[kPending]++
} else {
client.dispatch(opts, handler)
if (client.busy && this.busy) {
this[kNeedDrain] = true
}
this[kQueued]++
} else if (!dispatcher.dispatch(opts, handler)) {
dispatcher[kNeedDrain] = true
this[kNeedDrain] = this[kConnections] && this[kClients].length === this[kConnections]
}

@@ -197,2 +224,4 @@ } catch (err) {

}
return !this[kNeedDrain]
}

@@ -262,8 +291,2 @@

Pool.prototype.request = require('./client-request')
Pool.prototype.stream = require('./client-stream')
Pool.prototype.pipeline = require('./client-pipeline')
Pool.prototype.upgrade = require('./client-upgrade')
Pool.prototype.connect = require('./client-connect')
module.exports = Pool
{
"name": "undici",
"version": "3.3.4",
"version": "4.0.0-alpha.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"main": "index.js",
"types": "index.d.ts",
"module": "wrapper.mjs",
"scripts": {
"lint": "standard | snazzy",
"test": "tap test/*.js --no-coverage && jest test/jest/test",
"test:typescript": "tsd",
"coverage": "standard | snazzy && tap test/*.js",
"coverage:ci": "npm run coverage -- --coverage-report=lcovonly",
"bench": "npx concurrently -k -s first \"node benchmarks/server.js\" \"node -e 'setTimeout(() => {}, 1000)' && node benchmarks\""
"homepage": "https://github.com/nodejs/undici#readme",
"bugs": {
"url": "https://github.com/nodejs/undici/issues"
},

@@ -20,2 +13,3 @@ "repository": {

},
"license": "MIT",
"author": "Matteo Collina <hello@matteocollina.com>",

@@ -29,7 +23,25 @@ "contributors": [

],
"license": "MIT",
"bugs": {
"url": "https://github.com/nodejs/undici/issues"
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.(js|d.ts)",
"lib",
"types",
"docs"
],
"scripts": {
"prebuild:wasm": "docker build -t llhttp_wasm_builder -f build/Dockerfile .",
"build:wasm": "node build/wasm.js --docker",
"lint": "standard | snazzy",
"lint:fix": "standard --fix | snazzy",
"test": "tap test/*.js --no-coverage && jest test/jest/test",
"test:tdd": "tap test/*.js -w --no-coverage-report",
"test:typescript": "tsd",
"coverage": "standard | snazzy && tap test/*.js",
"coverage:ci": "npm run coverage -- --coverage-report=lcovonly",
"prebench": "node -e \"try { require('fs').unlinkSync(require('path').join(require('os').tmpdir(), 'undici.sock')) } catch (_) {}\"",
"bench": "npx concurrently -k -s first \"node benchmarks/server.js\" \"node -e 'setTimeout(() => {}, 1000)' && node benchmarks\"",
"serve:website": "docsify serve .",
"prepare": "husky install"
},
"homepage": "https://github.com/nodejs/undici#readme",
"devDependencies": {

@@ -40,15 +52,26 @@ "@sinonjs/fake-timers": "^6.0.1",

"benchmark": "^2.1.4",
"concurrently": "^5.2.0",
"concurrently": "^6.0.0",
"docsify-cli": "^4.4.2",
"https-pem": "^2.0.0",
"husky": "^6.0.0",
"jest": "^26.4.0",
"pre-commit": "^1.2.2",
"proxy": "^1.0.2",
"proxyquire": "^2.0.1",
"snazzy": "^8.0.0",
"standard": "^14.3.4",
"tap": "^14.10.8",
"tsd": "^0.13.1"
"semver": "^7.3.5",
"sinon": "^10.0.0",
"snazzy": "^9.0.0",
"standard": "^16.0.3",
"tap": "^15.0.0",
"tsd": "^0.14.0"
},
"pre-commit": [
"coverage"
],
"engines": {
"node": ">=12.18"
},
"standard": {
"ignore": [
"lib/llhttp/constants.js",
"lib/llhttp/utils.js"
]
},
"tsd": {

@@ -55,0 +78,0 @@ "directory": "test/types",

# undici
![Node CI](https://github.com/mcollina/undici/workflows/Node%20CI/badge.svg) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/master/graph/badge.svg)](https://codecov.io/gh/nodejs/undici)
[![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici)

@@ -10,5 +10,3 @@ A HTTP/1.1 client, written from scratch for Node.js.

<!--
Picture of Eleven
-->
Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel.

@@ -23,3 +21,3 @@ ## Install

Machine: AMD EPYC 7502P<br/>
Machine: AMD EPYC 7502P

@@ -60,646 +58,110 @@ Node 15

## API
## Common API Methods
<a name='client'></a>
### `new undici.Client(url, opts)`
This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
A basic HTTP/1.1 client, mapped on top of a single TCP/TLS connection. Pipelining is disabled
by default.
### `undici.request(url[, options]): Promise`
Requests are not guaranteeed to be dispatched in order of invocation.
Arguments:
`url` can be a string or a [`URL`](https://nodejs.org/api/url.html#url_class_url) object.
It should only include the protocol, hostname, and port.
* **url** `string | URL | object`
* **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcherdispatcher)
* **method** `String` - Default: `GET`
* **maxRedirections** `Integer` - Default: `0`
Options:
Returns a promise with the result of the `Dispatcher.request` method.
- `socketPath: String|Null`, an IPC endpoint, either Unix domain socket or Windows named pipe.
Default: `null`.
`url` may contain pathname. `options` may not contain path.
- `keepAliveTimeout: Number`, the timeout after which a socket without active requests
will time out. Monitors time between activity on a connected socket.
This value may be overridden by *keep-alive* hints from the server.
Default: `4e3` milliseconds (4s).
Calls `options.dispatcher.request(options)`.
- `keepAliveMaxTimeout: Number`, the maximum allowed `keepAliveTimeout` when overridden by
*keep-alive* hints from the server.
Default: `600e3` milliseconds (10min).
See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details.
- `keepAliveTimeoutThreshold: Number`, a number subtracted from server *keep-alive* hints
when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g.
transport latency.
Default: `1e3` milliseconds (1s).
### `undici.stream(url, options, factory): Promise`
- `headersTimeout: Number`, the timeout after which a request will time out, in
milliseconds. Monitors time between receiving complete headers.
Use `0` to disable it entirely. Default: `30e3` milliseconds (30s).
Arguments:
- `bodyTimeout: Number`, the timeout after which a request will time out, in
milliseconds. Monitors time between receiving body data.
Use `0` to disable it entirely. Default: `30e3` milliseconds (30s).
* **url** `string | URL | object`
* **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcherdispatcher)
* **method** `String` - Default: `GET`
* **factory** `Dispatcher.stream.factory`
- `pipelining: Number`, the amount of concurrent requests to be sent over the
single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2).
Carefully consider your workload and environment before enabling concurrent requests
as pipelining may reduce performance if used incorrectly. Pipelining is sensitive
to network stack settings as well as head of line blocking caused by e.g. long running requests.
Set to `0` to disable keep-alive connections.
Default: `1`.
Returns a promise with the result of the `Dispatcher.stream` method.
- `tls: Object|Null`, an options object which in the case of `https` will be passed to
[`tls.connect`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
Default: `null`.
`url` may contain pathname. `options` may not contain path.
- `maxHeaderSize: Number`, the maximum length of request headers in bytes.
Default: `16384` (16KiB).
Calls `options.dispatcher.stream(options, factory)`.
<a name='request'></a>
#### `client.request(opts[, callback(err, data)]): Promise|Void`
See [Dispatcher.stream](docs/api/Dispatcher.md#dispatcherstream) for more details.
Performs a HTTP request.
### `undici.pipeline(url, options, handler): Duplex`
Options:
Arguments:
* `path: String`
* `method: String`
* `opaque: Any`
* `body: String|Buffer|Uint8Array|stream.Readable|Null`
Default: `null`.
* `headers: Object|Array|Null`, an object with header-value pairs or an array with header-value pairs bi-indexed (`['header1', 'value1', 'header2', 'value2']`).
Default: `null`.
* `signal: AbortSignal|EventEmitter|Null`
Default: `null`.
* `idempotent: Boolean`, whether the requests can be safely retried or not.
If `false` the request won't be sent until all preceding
requests in the pipeline has completed.
Default: `true` if `method` is `HEAD` or `GET`.
* **url** `string | URL | object`
* **options** [`PipelineOptions`](docs/api/Dispatcher.md#parameter-pipelineoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcherdispatcher)
* **method** `String` - Default: `GET`
* **handler** `Dispatcher.pipeline.handler`
Headers are represented by an object like this:
Returns: `stream.Duplex`
```js
{
'content-length': '123',
'content-type': 'text/plain',
connection: 'keep-alive',
host: 'mysite.com',
accept: '*/*'
}
```
`url` may contain pathname. `options` may not contain path.
Or an array like this:
Calls `options.dispatch.pipeline(options, handler)`.
```js
[
'content-length', '123',
'content-type', 'text/plain',
'connection', 'keep-alive',
'host', 'mysite.com',
'accept', '*/*'
]
```
See [Dispatcher.pipeline](docs/api/Dispatcher.md#dispatcherpipeline) for more details.
Keys are lowercased. Values are not modified.
If you don't specify a `host` header, it will be derived from the `url` of the client instance.
### `undici.connect(options[, callback])`
The `data` parameter in `callback` is defined as follow:
Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
* `statusCode: Number`
* `opaque: Any`
* `headers: Object`, an object where all keys have been lowercased.
* `trailers: Object`, an object where all keys have been lowercased. This object start out
as empty and will be mutated to contain trailers after `body` has emitted `'end'`.
* `body: stream.Readable` response payload. A user **must**
either fully consume or destroy the body unless there is an error, or no further requests
will be processed.
Arguments:
Returns a promise if no callback is provided.
* **options** [`ConnectOptions`](docs/api/Dispatcher.md#parameter-connectoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcherdispatcher)
* **method** `String` - Default: `GET`
* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
Example:
Returns a promise with the result of the `Dispatcher.connect` method.
```js
const { Client } = require('undici')
const client = new Client(`http://localhost:3000`)
`url` may contain pathname. `options` may not contain path.
client.request({
path: '/',
method: 'GET'
}, function (err, data) {
if (err) {
// handle this in some way!
return
}
Calls `options.dispatch.connect(options)`.
const {
statusCode,
headers,
trailers,
body
} = data
See [Dispatcher.connect](docs/api/Dispatcher.md#dispatcherconnect) for more details.
console.log('response received', statusCode)
console.log('headers', headers)
### `undici.upgrade(options[, callback])`
body.setEncoding('utf8')
body.on('data', console.log)
body.on('end', () => {
console.log('trailers', trailers)
})
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
client.close()
})
```
Arguments:
Non-idempotent requests will not be pipelined in order
to avoid indirect failures.
* **options** [`UpgradeOptions`](docs/api/Dispatcher.md#parameter-upgradeoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcherdispatcher)
* **method** `String` - Default: `GET`
* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
Idempotent requests will be automatically retried if
they fail due to indirect failure from the request
at the head of the pipeline. This does not apply to
idempotent requests with a stream request body.
Returns a promise with the result of the `Dispatcher.upgrade` method.
##### Aborting a request
`url` may contain pathname. `options` may not contain path.
A request can be aborted using either an `AbortController` or an `EventEmitter`.
To use `AbortController` in Node.js versions earlier than 15, you will need to
install a shim - `npm i abort-controller`.
Calls `options.dispatcher.upgrade(options)`.
```js
const { Client } = require('undici')
See [Dispatcher.upgrade](docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
const client = new Client('http://localhost:3000')
const abortController = new AbortController()
### `undici.setGlobalDispatcher(dispatcher)`
client.request({
path: '/',
method: 'GET',
signal: abortController.signal
}, function (err, data) {
console.log(err) // RequestAbortedError
client.close()
})
* dispatcher `Dispatcher`
abortController.abort()
```
Sets the global dispatcher used by global API methods.
Alternatively, any `EventEmitter` that emits an `'abort'` event may be used as an abort controller:
### `undici.getGlobalDispatcher()`
```js
const EventEmitter = require('events')
const { Client } = require('undici')
Gets the global dispatcher used by global API methods.
const client = new Client('http://localhost:3000')
const ee = new EventEmitter()
Returns: `Dispatcher`
client.request({
path: '/',
method: 'GET',
signal: ee
}, function (err, data) {
console.log(err) // RequestAbortedError
client.close()
})
ee.emit('abort')
```
Destroying the request or response body will have the same effect.
<a name='stream'></a>
#### `client.stream(opts, factory(data)[, callback(err)]): Promise|Void`
A faster version of [`request`][request].
Unlike [`request`][request] this method expects `factory`
to return a [`Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) which the response will be
written to. This improves performance by avoiding
creating an intermediate [`Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) when the user
expects to directly pipe the response body to a
[`Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable).
Options:
* ... same as [`client.request(opts[, callback])`][request].
The `data` parameter in `factory` is defined as follow:
* `statusCode: Number`
* `headers: Object`, an object where all keys have been lowercased.
* `opaque: Any`
The `data` parameter in `callback` is defined as follow:
* `opaque: Any`
* `trailers: Object`, an object where all keys have been lowercased.
Returns a promise if no callback is provided.
```js
const { Client } = require('undici')
const client = new Client(`http://localhost:3000`)
const fs = require('fs')
client.stream({
path: '/',
method: 'GET',
opaque: filename
}, ({ statusCode, headers, opaque: filename }) => {
console.log('response received', statusCode)
console.log('headers', headers)
return fs.createWriteStream(filename)
}, (err) => {
if (err) {
console.error('failure', err)
} else {
console.log('success')
}
})
```
`opaque` makes it possible to avoid creating a closure
for the `factory` method:
```js
function (req, res) {
return client.stream({ ...opts, opaque: res }, proxy)
}
```
Instead of:
```js
function (req, res) {
return client.stream(opts, (data) => {
// Creates closure to capture `res`.
proxy({ ...data, opaque: res })
}
}
```
<a name='pipeline'></a>
#### `client.pipeline(opts, handler(data)): Duplex`
For easy use with [`stream.pipeline`](https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback).
Options:
* ... same as [`client.request(opts, callback)`][request].
* `objectMode: Boolean`, `true` if the `handler` will return an object stream.
Default: `false`
The `data` parameter in `handler` is defined as follow:
* `statusCode: Number`
* `headers: Object`, an object where all keys have been lowercased.
* `opaque: Any`
* `body: stream.Readable` response payload. A user **must**
either fully consume or destroy the body unless there is an error, or no further requests
will be processed.
`handler` should return a [`Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable) from which the result will be
read. Usually it should just return the `body` argument unless
some kind of transformation needs to be performed based on e.g.
`headers` or `statusCode`.
The `handler` should validate the response and save any
required state. If there is an error it should be thrown.
Returns a `Duplex` which writes to the request and reads from
the response.
```js
const { Client } = require('undici')
const client = new Client(`http://localhost:3000`)
const fs = require('fs')
const stream = require('stream')
stream.pipeline(
fs.createReadStream('source.raw'),
client.pipeline({
path: '/',
method: 'PUT',
}, ({ statusCode, headers, body }) => {
if (statusCode !== 201) {
throw new Error('invalid response')
}
if (isZipped(headers)) {
return pipeline(body, unzip(), () => {})
}
return body
}),
fs.createWriteStream('response.raw'),
(err) => {
if (err) {
console.error('failed')
} else {
console.log('succeeded')
}
}
)
```
<a name='upgrade'></a>
#### `client.upgrade(opts[, callback(err, data)]): Promise|Void`
Upgrade to a different protocol.
Options:
* `path: String`
* `opaque: Any`
* `method: String`
Default: `GET`
* `headers: Object|Null`, an object with header-value pairs.
Default: `null`
* `signal: AbortSignal|EventEmitter|Null`.
Default: `null`
* `protocol: String`, a string of comma separated protocols, in descending preference order.
Default: `Websocket`.
The `data` parameter in `callback` is defined as follow:
* `headers: Object`, an object where all keys have been lowercased.
* `socket: Duplex`
* `opaque`
Returns a promise if no callback is provided.
<a name='connect'></a>
#### `client.connect(opts[, callback(err, data)]): Promise|Void`
Starts two-way communications with the requested resource.
Options:
* `path: String`
* `opaque: Any`
* `headers: Object|Null`, an object with header-value pairs.
Default: `null`
* `signal: AbortSignal|EventEmitter|Null`.
Default: `null`
The `data` parameter in `callback` is defined as follow:
* `statusCode: Number`
* `headers: Object`, an object where all keys have been lowercased.
* `socket: Duplex`
* `opaque: Any`
Returns a promise if no callback is provided.
<a name='dispatch'></a>
#### `client.dispatch(opts, handler): Void`
This is the low level API which all the preceding APIs are implemented on top of.
This API is expected to evolve through semver-major versions and is less stable
than the preceding higher level APIs. It is primarily intended for library developers
who implement higher level APIs on top of this.
Multiple handler methods may be invoked in the same tick.
Options:
* `path: String`
* `method: String`
* `body: String|Buffer|Uint8Array|stream.Readable|Null`
Default: `null`.
* `headers: Object|Null`, an object with header-value pairs.
Default: `null`.
* `idempotent: Boolean`, whether the requests can be safely retried or not.
If `false` the request won't be sent until all preceding
requests in the pipeline has completed.
Default: `true` if `method` is `HEAD` or `GET`.
The `handler` parameter is defined as follow:
* `onConnect(abort)`, invoked before request is dispatched on socket.
May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
* `abort(): Void`, abort request.
* `onUpgrade(statusCode, headers, socket): Void`, invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method.
* `statusCode: Number`
* `headers: Array|Null`
* `socket: Duplex`
* `onHeaders(statusCode, headers, resume): Boolean`, invoked when statusCode and headers have been received.
May be invoked multiple times due to 1xx informational headers.
* `statusCode: Number`
* `headers: Array|Null`, an array of key-value pairs. Keys are not automatically lowercased.
* `resume(): Void`, resume `onData` after returning `false`.
* `onData(chunk): Boolean`, invoked when response payload data is received.
* `chunk: Buffer`
* `onComplete(trailers): Void`, invoked when response payload and trailers have been received and the request has completed.
* `trailers: Array|Null`
* `onError(err): Void`, invoked when an error has occurred.
* `err: Error`
The caller is responsible for handling the `body` argument, in terms of `'error'` events and `destroy()`:ing up until
the `onConnect` handler has been invoked.
<a name='close'></a>
#### `client.close([callback]): Promise|Void`
Closes the client and gracefully waits for enqueued requests to
complete before invoking the callback.
Returns a promise if no callback is provided.
<a name='destroy'></a>
#### `client.destroy([err][, callback]): Promise|Void`
Destroy the client abruptly with the given `err`. All the pending and running
requests will be asynchronously aborted and error. Waits until socket is closed
before invoking the callback. Since this operation is asynchronously dispatched
there might still be some progress on dispatched requests.
Returns a promise if no callback is provided.
#### `client.url: URL`
Returns url passed to `undici.Pool(url, opts)`.
#### `client.pipelining: Number`
Property to get and set the pipelining factor.
#### `client.pending: Number`
Number of queued requests.
#### `client.running: Number`
Number of inflight requests.
#### `client.size: Number`
Number of pending and running requests.
#### `client.connected: Boolean|Integer`
Thruthy if the client has an active connection. The client will lazily
create a connection when it receives a request and will destroy it
if there is no activity for the duration of the `timeout` value.
#### `client.busy: Boolean`
True if pipeline is saturated or blocked. Indicates whether dispatching
further requests is meaningful.
#### `client.closed: Boolean`
True after `client.close()` has been called.
#### `client.destroyed: Boolean`
True after `client.destroyed()` has been called or `client.close()` has been
called and the client shutdown has completed.
#### Events
* `'drain'`, emitted when pipeline is no longer fully
saturated.
* `'connect'`, emitted when a socket has been created and
connected. The client will connect once `client.size > 0`.
* `'disconnect'`, emitted when socket has disconnected. The
first argument of the event is the error which caused the
socket to disconnect. The client will reconnect if or once
`client.size > 0`.
<a name='pool'></a>
### `new undici.Pool(url, opts)`
A pool of [`Client`][] connected to the same upstream target.
Implements the same api as [`Client`][] with a few minor
differences.
Requests are not guaranteeed to be dispatched in order of invocation.
Options:
* ... same as [`Client`][].
* `connections`, the number of clients to create.
Default `10`.
#### Events
* `'connect'`, emitted when a client has connected. The first argument is the
`Client` instance
* `'disconnect'`, emitted when a client has disconnected. The first argument is the
`Client` instance, the second is the the error that caused the disconnection.
<a name='agent'></a>
### `new undici.Agent(opts)`
* opts `undici.Pool.options` - options passed through to Pool constructor
Returns: `Agent`
Returns a new Agent instance for use with pool based requests or the following top-level methods `request`, `pipeline`, and `stream`.
#### `agent.get(origin): Pool`
* origin `string` - A pool origin to be retrieved from the Agent.
This method retrieves Pool instances from the Agent. If the pool does not exist it is automatically added. You do not need to manually close these pools as they are automatically removed using a WeakCache based on WeakRef and FinalizationRegistry.
The following methods `request`, `pipeline`, and `stream` utilize this feature.
#### `agent.close(): Promise`
Returns a `Promise.all` operation closing all of the pool instances in the Agent instance. This calls `pool.close` under the hood.
#### `agent.destroy(): Promise`
Returns a `Promise.all` operation destroying all of the pool instances in the Agent instance. This calls `pool.destroy` under the hood.
### `undici.setGlobalAgent(agent)`
* agent `Agent`
Sets the global agent used by `request`, `pipeline`, and `stream` methods.
The default global agent creates `undici.Pool`s with no max number of
connections.
The agent must only **implement** the `Agent` API; not necessary extend from it.
### `undici.request(url[, opts]): Promise`
* url `string | URL | object`
* opts `{ agent: Agent } & client.request.opts`
`url` may contain path. `opts` may not contain path. `opts.method` is `GET` by default.
Calls `pool.request(opts)` on the pool returned from either the globalAgent (see [setGlobalAgent](#undicisetglobalagentagent)) or the agent passed to the `opts` argument.
Returns a promise with the result of the `request` method.
### `undici.stream(url, opts, factory): Promise`
* url `string | URL | object`
* opts `{ agent: Agent } & client.stream.opts`
* factory `client.stream.factory`
`url` may contain path. `opts` may not contain path.
See [client.stream](#clientstreamopts-factorydata-callbackerr-promisevoid) for details on the `opts` and `factory` arguments.
Calls `pool.stream(opts, factory)` on the pool returned from either the globalAgent (see [setGlobalAgent](#undicisetglobalagentagent)) or the agent passed to the `opts` argument.
Result is returned in the factory function. See [client.stream](#clientstreamopts-factorydata-callbackerr-promisevoid) for more details.
### `undici.pipeline(url, opts, handler): Duplex`
* url `string | URL | object`
* opts `{ agent: Agent } & client.pipeline.opts`
* handler `client.pipeline.handler`
`url` may contain path. `opts` may not contain path.
See [client.pipeline](#clientpipelineopts-handlerdata-duplex) for details on the `opts` and `handler` arguments.
Calls `pool.pipeline(opts, factory)` on the pool returned from either the globalAgent (see [setGlobalAgent](#undicisetglobalagentagent)) or the agent passed to the `opts` argument.
See [client.pipeline](#clientpipelineopts-handlerdata-duplex) for more details.
### `client.upgrade(opts[, callback(err, data)]): Promise|Void`
* url `string | URL | object`
* opts `{ agent: Agent } & client.upgrade.opts`
`url` may contain path. `opts` may not contain path.
### `client.connect(opts[, callback(err, data)]): Promise|Void`
* url `string | URL | object`
* opts `{ agent: Agent } & client.connect.opts`
`url` may contain path. `opts` may not contain path.
<a name='errors'></a>
### `undici.errors`
Undici exposes a variety of error objects that you can use to enhance your error handling.
You can find all the error objects inside the `errors` key.
```js
const { errors } = require('undici')
```
| Error | Error Codes | Description |
| -----------------------------|-----------------------------------|------------------------------------------------|
| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. |
| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. |
| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user |
| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. |
| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. |
| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. |
| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. |
| `ContentLengthMismatchError` | `UND_ERR_CONTENT_LENGTH_MISMATCH`| body does not match content-length header |
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
| `TrailerMismatchError` | `UND_ERR_TRAILER_MISMATCH` | trailers did not match specification |
## Specification Compliance

@@ -710,3 +172,3 @@

#### Expect
### Expect

@@ -733,7 +195,13 @@ Undici does not support the `Expect` request header field. The request

Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2<br/>
Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
Undici will abort all running requests in the pipeline when any of them are
aborted.
* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
## Collaborators
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
* [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>

@@ -744,9 +212,1 @@

MIT
[`Client`]: #client
[request]: #request
[stream]: #stream
[pipeline]: #pipeline
[upgrade]: #upgrade
[connect]: #connect
[dispatch]: #dispatch

@@ -1,37 +0,29 @@

import { UrlObject } from 'url'
import { URL } from 'url'
import Dispatcher from './dispatcher'
import Pool from './pool'
import Client from './client'
import { Duplex } from 'stream'
import { URL } from 'url'
export {
Agent,
setGlobalAgent,
request,
stream,
pipeline,
}
export = Agent
declare class Agent {
constructor(opts?: Pool.Options)
get(origin: string): Pool;
declare class Agent extends Dispatcher{
constructor(opts?: Agent.Options)
/** `true` after `dispatcher.close()` has been called. */
closed: boolean;
/** `true` after `dispatcher.destroyed()` has been called or `dispatcher.close()` has been called and the dispatcher shutdown has completed. */
destroyed: boolean;
/** Dispatches a request. */
dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): void;
}
declare function setGlobalAgent<AgentImplementation extends Agent>(agent: AgentImplementation): void;
declare namespace Agent {
export interface Options extends Pool.Options {
/** Default: `(origin, opts) => new Pool(origin, opts)`. */
factory?(origin: URL, opts: Object): Dispatcher;
/** Integer. Default: `0` */
maxRedirections?: number;
}
declare function request(
url: string | URL | UrlObject,
opts?: { agent?: Agent } & Client.RequestOptions,
): PromiseLike<Client.ResponseData>;
declare function stream(
url: string | URL | UrlObject,
opts: { agent?: Agent } & Client.RequestOptions,
factory: Client.StreamFactory
): PromiseLike<Client.StreamData>;
declare function pipeline(
url: string | URL | UrlObject,
opts: { agent?: Agent } & Client.PipelineOptions,
handler: Client.PipelineHandler
): Duplex;
export interface DispatchOptions extends Dispatcher.DispatchOptions {
/** Integer. */
maxRedirections?: number;
}
}
import { URL } from 'url'
import { TlsOptions } from 'tls'
import { Duplex, Readable, Writable } from 'stream'
import { EventEmitter } from 'events'
import { IncomingHttpHeaders } from 'http'
import Dispatcher from './dispatcher'
type AbortSignal = unknown;
export = Client
/** A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. */
declare class Client extends EventEmitter {
declare class Client extends Dispatcher {
constructor(url: string | URL, options?: Client.Options);
/** Property to get and set the pipelining factor. */
pipelining: number;
/** Number of queued requests. */
pending: number;
/** Number of inflight requests. */
running: number;
/** Number of pending and running requests. */
size: number;
/** True if the client has an active connection. The client will lazily create a connection when it receives a request and will destroy it if there is no activity for the duration of the `timeout` value. */
connected: boolean;
/** True if pipeline is saturated or blocked. Indicates whether dispatching further requests is meaningful. */
busy: boolean;
/** True after `client.close()` has been called. */
/** `true` after `client.close()` has been called. */
closed: boolean;
/** True after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed. */
/** `true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed. */
destroyed: boolean;
/** Performs a HTTP request */
request(options: Client.RequestOptions): PromiseLike<Client.ResponseData>;
request(options: Client.RequestOptions, callback: (err: Error | null, data: Client.ResponseData) => void): void;
/** A faster version of `Client.request` */
stream(options: Client.RequestOptions, factory: Client.StreamFactory): PromiseLike<Client.StreamData>;
stream(options: Client.RequestOptions, factory: Client.StreamFactory, callback: (err: Error | null, data: Client.StreamData) => void): void;
/** For easy use with `stream.pipeline` */
pipeline(options: Client.PipelineOptions, handler: Client.PipelineHandler): Duplex;
/** Upgrade to a different protocol */
upgrade(options: Client.UpgradeOptions): PromiseLike<Client.UpgradeData>;
upgrade(options: Client.UpgradeOptions, callback: (err: Error | null, data: Client.UpgradeData) => void): void;
/** Starts two-way communications with the requested resource */
connect(options: Client.ConnectOptions): PromiseLike<Client.ConnectData>;
connect(options: Client.ConnectOptions, callback: (err: Error | null, data: Client.ConnectData) => void): void;
/** This is the low level API which all the preceding APIs are implemented on top of. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this. */
dispatch(options: Client.DispatchOptions, handlers: Client.DispatchHandlers): void;
/** Closes the client and gracefully waits for enqueued requests to complete before invoking the callback (or returnning a promise if no callback is provided). */
close(): PromiseLike<void>;
close(callback: () => void): void;
/** Destroy the client abruptly with the given err. All the pending and running requests will be asynchronously aborted and error. Waits until socket is closed before invoking the callback (or returnning a promise if no callback is provided). Since this operation is asynchronously dispatched there might still be some progress on dispatched requests. */
destroy(): PromiseLike<void>;
destroy(err: Error | null): PromiseLike<void>;
destroy(callback: () => void): void;
destroy(err: Error | null, callback: () => void): void;
}

@@ -69,122 +22,21 @@

socketPath?: string | null;
/** the timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overriden by *keep-alive* hints from the server. Default: `4e3` milliseconds (4s). */
keepAliveTimeout?: number;
/** the maximum allowed `idleTimeout` when overriden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */
keepAliveMaxTimeout?: number;
/** A number subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuries caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */
keepAliveTimeoutThreshold?: number;
/** the timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. Default: `4e3` milliseconds (4s). */
keepAliveTimeout?: number | null;
/** the maximum allowed `idleTimeout` when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */
keepAliveMaxTimeout?: number | null;
/** A number subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */
keepAliveTimeoutThreshold?: number | null;
/** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Default: `1`. */
pipelining?: number;
pipelining?: number | null;
/** An options object which in the case of `https` will be passed to [`tls.connect`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback). Default: `null`. */
tls?: TlsOptions | null;
/** The maximum length of request headers in bytes. Default: `16384` (16KiB). */
maxHeaderSize?: number;
maxHeaderSize?: number | null;
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `30e3` milliseconds (30s). */
bodyTimeout?: number | null;
/** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `30e3` milliseconds (30s). */
headersTimeout?: number;
headersTimeout?: number | null;
/** If `true`, an error is thrown when the request content-length header doesn't match the length of the request body. Default: `true`. */
strictContentLength?: boolean
}
export interface DispatchOptions {
path: string;
method: string;
/** Default: `null` */
body?: string | Buffer | Uint8Array | Readable | null;
/** Default: `null` */
headers?: IncomingHttpHeaders | null;
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving a complete headers. Use 0 to disable it entirely. Default: `30e3` milliseconds (30s). */
headersTimeout?: number;
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving a body data. Use 0 to disable it entirely. Default: `30e3` milliseconds (30s). */
bodyTimeout?: number;
/** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceeding requests in the pipeline has completed. Default: `true` if `method` is `HEAD` or `GET`. */
idempotent?: boolean;
}
export interface RequestOptions extends DispatchOptions {
opaque?: unknown;
/** Default: `null` */
signal?: AbortSignal | EventEmitter | null;
}
export interface PipelineOptions extends RequestOptions {
/** `true` if the `handler` will return an object stream. Default: `false` */
objectMode?: boolean;
}
export interface UpgradeOptions {
path: string;
method?: string;
/** Default: `null` */
headers?: IncomingHttpHeaders | null;
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving a complete headers. Use 0 to disable it entirely. Default: `30e3` milliseconds (30s). */
headersTimeout?: number;
/** A string of comma separated protocols, in descending preference order. Default: `'Websocket'` */
protocol?: string;
/** Default: `null` */
signal?: AbortSignal | EventEmitter | null;
}
export interface ConnectOptions {
path: string;
/** Default: `null` */
headers?: IncomingHttpHeaders | null;
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving a complete headers. Use 0 to disable it entirely. Default: `30e3` milliseconds (30s). */
headersTimeout?: number;
/** Default: `null` */
signal?: AbortSignal | EventEmitter | null;
}
export interface ResponseData {
statusCode: number;
headers: IncomingHttpHeaders;
body: Readable;
opaque?: unknown;
}
export interface StreamData {
opaque: unknown;
trailers: Record<string, unknown>;
}
export interface UpgradeData {
headers: IncomingHttpHeaders;
socket: Duplex;
opaque: unknown;
}
export interface ConnectData {
statusCode: number;
headers: IncomingHttpHeaders;
socket: Duplex;
opaque: unknown;
}
export interface StreamFactoryData {
statusCode: number;
headers: IncomingHttpHeaders;
opaque: unknown;
}
export type StreamFactory = (data: StreamFactoryData) => Writable
export interface PipelineHandlerData {
statusCode: number;
headers: IncomingHttpHeaders;
opaque: unknown;
body: Readable;
}
export type PipelineHandler = (data: PipelineHandlerData) => Readable
export interface DispatchHandlers {
/** Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. */
onConnect?(abort: () => void): void;
/** Invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method */
onUpgrade?(statusCode: number, headers: string[] | null, socket: Duplex): void;
/** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */
onHeaders?(statusCode: number, headers: string[] | null, resume: () => void): boolean;
/** Invoked when response payload data is received */
onData?(chunk: Buffer): boolean;
/** Invoked when response payload and trailers have been received and the request has completed. */
onComplete?(trailers: string[] | null): void;
/** Invoked when an error has occurred. */
onError?(err: Error): void;
}
}
import Client from './client'
import Dispatcher from './dispatcher'
import { URL } from 'url'

@@ -6,4 +7,8 @@

declare class Pool extends Client {
declare class Pool extends Dispatcher {
constructor(url: string | URL, options?: Pool.Options)
/** `true` after `pool.close()` has been called. */
closed: boolean;
/** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */
destroyed: boolean;
}

@@ -13,5 +18,7 @@

export interface Options extends Client.Options {
/** The number of clients to create. Default `100`. */
connections?: number
/** Default: `(origin, opts) => new Client(origin, opts)`. */
factory?(origin: URL, opts: object): Dispatcher;
/** The max number of clients to create. `null` if no limit. Default `null`. */
connections?: number | null;
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc