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.2.0 to 3.3.0

83

lib/agent.js
'use strict'
/* global WeakRef, FinalizationRegistry */
const { InvalidArgumentError, InvalidReturnValueError } = require('./core/errors')
const Pool = require('./pool')
const { weakCache } = require('./core/util')
const util = require('./core/util')
const { kAgentOpts, kAgentCache, kAgentCleanup } = require('./core/symbols')
const kAgentFactory = Symbol('agent factory')
class Agent {
constructor (opts) {
this[kAgentFactory] = weakCache(origin => new Pool(origin, opts))
this[kAgentOpts] = opts
this[kAgentCache] = new Map()
this[kAgentCleanup] = new FinalizationRegistry(key => {
// get the WeakRef from the cache
const ref = this[kAgentCache].get(key)
// if the WeakRef exists and the object has been reclaimed
if (ref !== undefined && ref.deref() === undefined) {
// remove the WeakRef from the cache
this[kAgentCache].delete(key)
}
})
}

@@ -19,4 +29,46 @@

return this[kAgentFactory](origin)
// check the cache for an existing WeakRef
const ref = this[kAgentCache].get(origin)
// if one exists in the cache try to return the WeakRef
if (ref !== undefined) {
const cached = ref.deref()
if (cached !== undefined) {
return cached
}
}
// otherwise, if it isn't in the cache or the reference has been cleaned up, create a new one!
const value = new Pool(origin, this[kAgentOpts])
// add a WeakRef of the value to the cache
this[kAgentCache].set(origin, new WeakRef(value))
// add the value to the finalization registry
this[kAgentCleanup].register(value, origin)
return value
}
close () {
const closePromises = []
for (const ref of this[kAgentCache].values()) {
const pool = ref.deref()
if (pool) {
closePromises.push(pool.close())
}
}
return Promise.all(closePromises)
}
destroy () {
const destroyPromises = []
for (const ref of this[kAgentCache].values()) {
const pool = ref.deref()
if (pool) {
destroyPromises.push(pool.destroy())
}
}
return Promise.all(destroyPromises)
}
}

@@ -40,10 +92,2 @@

return (url, { agent = globalAgent, method = 'GET', ...opts } = {}, ...additionalArgs) => {
if (url === undefined || url === null) {
throw new InvalidArgumentError('Argument url must be defined')
}
if (!agent || typeof agent.get !== 'function') {
throw new InvalidArgumentError('Argument agent must implement Agent')
}
if (opts.path != null) {

@@ -53,11 +97,8 @@ throw new InvalidArgumentError('unsupported opts.path')

// if url is a string transform into URL inst. otherwise use as is
url = typeof url === 'string' ? new URL(url) : url
// ensure it atleast has a non-empty string origin
if (!url || typeof url.origin !== 'string' || url.origin === '') {
throw new InvalidArgumentError('Argument url.origin must be a non-empty string')
}
const { origin, pathname, search } = util.parseURL(url)
const client = agent.get(url.origin)
const path = `${pathname || '/'}${search || ''}`
const client = agent.get(origin)
if (client && typeof client[requestType] !== 'function') {

@@ -67,4 +108,2 @@ throw new InvalidReturnValueError(`Client returned from Agent.get() does not implement method ${requestType}`)

const path = url.path || `${url.pathname || '/'}${url.search || ''}`
return client[requestType]({ ...opts, method, path }, ...additionalArgs)

@@ -71,0 +110,0 @@ }

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

const { addSignal, removeSignal } = require('./abort-signal')
const assert = require('assert')

@@ -41,2 +42,4 @@ class UpgradeHandler extends AsyncResource {

assert.strictEqual(statusCode, 101)
removeSignal(this)

@@ -43,0 +46,0 @@

'use strict'
const { URL } = require('url')
const net = require('net')

@@ -111,14 +110,6 @@ const tls = require('tls')

if (typeof url === 'string') {
url = new URL(url)
if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) {
throw new InvalidArgumentError('invalid maxHeaderSize')
}
if (!url || typeof url !== 'object') {
throw new InvalidArgumentError('invalid url')
}
if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
throw new InvalidArgumentError('invalid port')
}
if (socketPath != null && typeof socketPath !== 'string') {

@@ -128,18 +119,2 @@ throw new InvalidArgumentError('invalid socketPath')

if (url.hostname != null && typeof url.hostname !== 'string') {
throw new InvalidArgumentError('invalid hostname')
}
if (!/https?/.test(url.protocol)) {
throw new InvalidArgumentError('invalid protocol')
}
if (/\/.+/.test(url.pathname) || url.search || url.hash) {
throw new InvalidArgumentError('invalid url')
}
if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) {
throw new InvalidArgumentError('invalid maxHeaderSize')
}
if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) {

@@ -168,3 +143,3 @@ throw new InvalidArgumentError('invalid keepAliveTimeout')

this[kMaxHeadersSize] = maxHeaderSize || 16384
this[kUrl] = url
this[kUrl] = util.parseOrigin(url)
this[kSocketPath] = socketPath

@@ -202,2 +177,6 @@ this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout

get url () {
return this[kUrl]
}
get pipelining () {

@@ -204,0 +183,0 @@ return this[kPipelining]

@@ -34,3 +34,6 @@ module.exports = {

kTLSSession: Symbol('tls session cache'),
kHostHeader: Symbol('host header')
kHostHeader: Symbol('host header'),
kAgentOpts: Symbol('agent opts'),
kAgentCache: Symbol('agent cache'),
kAgentCleanup: Symbol('agent cleanup')
}
'use strict'
/* global WeakRef, FinalizationRegistry */

@@ -9,3 +8,3 @@ const assert = require('assert')

const net = require('net')
const { NotSupportedError } = require('./errors')
const { InvalidArgumentError } = require('./errors')

@@ -18,2 +17,46 @@ function nop () {}

function parseURL (url) {
if (typeof url === 'string') {
url = new URL(url)
}
if (!url || typeof url !== 'object') {
throw new InvalidArgumentError('invalid url')
}
if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
throw new InvalidArgumentError('invalid port')
}
if (url.hostname != null && typeof url.hostname !== 'string') {
throw new InvalidArgumentError('invalid hostname')
}
if (!/https?/.test(url.protocol)) {
throw new InvalidArgumentError('invalid protocol')
}
if (!(url instanceof URL)) {
const port = url.port || {
'http:': 80,
'https:': 443
}[url.protocol]
assert(port != null)
const path = url.path || `${url.pathname || '/'}${url.search || ''}`
url = new URL(`${url.protocol}//${url.hostname}:${port}${path}`)
}
return url
}
function parseOrigin (url) {
url = parseURL(url)
if (/\/.+/.test(url.pathname) || url.search || url.hash) {
throw new InvalidArgumentError('invalid url')
}
return url
}
function getServerName (host) {

@@ -116,43 +159,6 @@ if (!host) {

/* istanbul ignore next: https://github.com/tc39/proposal-weakrefs */
function weakCache (fn) {
/* istanbul ignore next: */
if (typeof WeakRef === 'undefined' || typeof FinalizationRegistry === 'undefined') {
throw new NotSupportedError('In order to use this feature, `WeakRef` and `FinalizationRegistry` must be defined as global objects. Check your Node.js version to be sure it is v14.6.0 or greater.')
}
const cache = new Map()
const cleanup = new FinalizationRegistry(key => {
// get the WeakRef from the cache
const ref = cache.get(key)
// if the WeakRef exists and the object has been reclaimed
if (ref !== undefined && ref.deref() === undefined) {
// remove the WeakRef from the cache
cache.delete(key)
}
})
return key => {
// check the cache for an existing WeakRef
const ref = cache.get(key)
// if one exists in the cache try to return the WeakRef
if (ref !== undefined) {
const cached = ref.deref()
if (cached !== undefined) {
return cached
}
}
// otherwise, if it isn't in the cache or the reference has been cleaned up, create a new one!
const value = fn(key)
// add a WeakRef of the value to the cache
cache.set(key, new WeakRef(value))
// add the value to the finalization registry
cleanup.register(value, key)
return value
}
}
module.exports = {
nop,
parseOrigin,
parseURL,
getServerName,

@@ -166,4 +172,3 @@ errnoException,

bodyLength,
isBuffer,
weakCache
isBuffer
}

@@ -11,4 +11,6 @@ 'use strict'

const FixedQueue = require('./node/fixed-queue')
const util = require('./core/util')
const kClients = Symbol('clients')
const kNeedDrain = Symbol('needDrain')
const kQueue = Symbol('queue')

@@ -23,8 +25,11 @@ const kDestroyed = Symbol('destroyed')

const kOnDisconnect = Symbol('onDisconnect')
const kPending = Symbol('pending')
const kConnected = Symbol('connected')
const kConnections = Symbol('connections')
class Pool extends EventEmitter {
constructor (url, options = {}) {
constructor (origin, opts = {}) {
super()
const { connections } = options
const { connections, ...options } = opts

@@ -35,3 +40,4 @@ if (connections != null && (!Number.isFinite(connections) || connections < 0)) {

this[kUrl] = url
this[kConnections] = connections || null
this[kUrl] = util.parseOrigin(origin)
this[kOptions] = JSON.parse(JSON.stringify(options))

@@ -43,2 +49,5 @@ this[kQueue] = new FixedQueue()

this[kClients] = []
this[kNeedDrain] = false
this[kPending] = 0
this[kConnected] = 0

@@ -55,6 +64,12 @@ const pool = this

}
pool[kPending]--
this.dispatch(item.opts, item.handler)
}
if (pool[kClosedResolve] && queue.isEmpty()) {
if (pool[kNeedDrain] && !pool[kPending]) {
pool[kNeedDrain] = false
pool.emit('drain')
}
if (pool[kClosedResolve] && !pool[kPending]) {
Promise

@@ -67,10 +82,40 @@ .all(pool[kClients].map(c => c.close()))

this[kOnConnect] = function onConnect () {
pool[kConnected]++
pool.emit('connect', this)
}
this[kOnDisconnect] = function onDisconnect () {
pool.emit('disconnect', this)
this[kOnDisconnect] = function onDisconnect (err) {
pool[kConnected]--
pool.emit('disconnect', this, err)
}
}
get url () {
return this[kUrl]
}
get connected () {
return this[kConnected]
}
get busy () {
return this[kPending] > 0
}
get pending () {
return this[kPending]
}
// TODO: get running () {}
// TODO: get size () {}
get destroyed () {
return this[kDestroyed]
}
get closed () {
return this[kClosedPromise] != null
}
dispatch (opts, handler) {

@@ -89,5 +134,4 @@ try {

if (!client) {
const { connections, ...options } = this[kOptions]
if (!connections || this[kClients].length < connections) {
client = new Client(this[kUrl], options)
if (!this[kConnections] || this[kClients].length < this[kConnections]) {
client = new Client(this[kUrl], this[kOptions])
.on('drain', this[kOnDrain])

@@ -102,3 +146,5 @@ .on('connect', this[kOnConnect])

if (!client) {
this[kNeedDrain] = true
this[kQueue].push({ opts, handler })
this[kPending]++
} else {

@@ -130,2 +176,5 @@ client.dispatch(opts, handler)

}
this[kClosedPromise] = this[kClosedPromise].then(() => {
this[kDestroyed] = true
})
}

@@ -132,0 +181,0 @@

{
"name": "undici",
"version": "3.2.0",
"version": "3.3.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -52,3 +52,3 @@ # undici

for await (const data of body) {
console.log('data', chunk)
console.log('data', data)
}

@@ -511,2 +511,6 @@

#### `client.url: URL`
Returns url passed to `undici.Pool(url, opts)`.
#### `client.pipelining: Number`

@@ -575,2 +579,28 @@

#### `pool.url: URL`
Returns url passed to `undici.Pool(url, opts)`.
#### `pool.connected: Integer`
Number of active connections in pool.
#### `pool.pending: Number`
Number of queued requests.
#### `pool.busy: Boolean`
True if pool is saturated or blocked. Indicates whether dispatching
further requests is meaningful.
#### `pool.closed: Boolean`
True after `pool.close()` has been called.
#### `pool.destroyed: Boolean`
True after `pool.destroyed()` has been called or `pool.close()` has been
called and the client shutdown has completed.
#### `pool.request(opts[, callback]): Promise|Void`

@@ -610,31 +640,12 @@

* `'drain'`, emitted when pool is no longer fully
saturated.
* `'connect'`, emitted when a client has connected, the `Client`
instance is passed as argument.
* `'disconnect'`, emitted when a client has disconnected, the `Client`
instance is passed as argument.
* `'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='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 |
<a name='agent'></a>
### `new undici.Agent(opts)`

@@ -726,2 +737,25 @@

<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

@@ -728,0 +762,0 @@

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

const { InvalidArgumentError, InvalidReturnValueError } = require('../lib/core/errors')
const { errors } = require('..')

@@ -13,3 +14,3 @@ const SKIP = typeof WeakRef === 'undefined' || typeof FinalizationRegistry === 'undefined'

tap.test('Agent', { skip: SKIP }, t => {
t.plan(5)
t.plan(6)

@@ -45,2 +46,85 @@ t.test('setGlobalAgent', t => {

t.test('Agent close and destroy', t => {
t.plan(2)
t.test('agent should close internal pools', t => {
t.plan(2)
const wanted = 'payload'
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.tearDown(server.close.bind(server))
server.listen(0, () => {
const agent = new Agent()
const origin = `http://localhost:${server.address().port}`
request(origin, { agent })
.then(() => {
t.pass('first request should resolve')
})
.catch(err => {
t.fail(err)
})
const pool = agent.get(origin)
pool.once('connect', () => {
agent.close().then(() => {
request(origin, { agent })
.then(() => {
t.fail('second request should not resolve')
})
.catch(err => {
t.error(err instanceof errors.ClientClosedError)
})
})
})
})
})
t.test('agent should destroy internal pools', t => {
t.plan(2)
const wanted = 'payload'
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.tearDown(server.close.bind(server))
server.listen(0, () => {
const agent = new Agent()
const origin = `http://localhost:${server.address().port}`
request(origin, { agent })
.then(() => {
t.fail()
})
.catch(err => {
t.ok(err instanceof errors.ClientDestroyedError)
})
const pool = agent.get(origin)
pool.once('connect', () => {
agent.destroy().then(() => {
request(origin, { agent })
.then(() => {
t.fail()
})
.catch(err => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
})
})
})
})
t.test('request a resource', t => {

@@ -47,0 +131,0 @@ t.plan(5)

@@ -12,3 +12,3 @@ 'use strict'

test('basic get', (t) => {
t.plan(23)
t.plan(24)

@@ -38,2 +38,4 @@ const server = createServer((req, res) => {

t.strictEqual(client.url.origin, `http://localhost:${server.address().port}`)
const signal = new EE()

@@ -621,3 +623,3 @@ client.request({

port: server.address().port,
protocol: 'http'
protocol: 'http:'
})

@@ -648,3 +650,3 @@ t.tearDown(client.close.bind(client))

port: server.address().port,
protocol: 'http'
protocol: 'http:'
})

@@ -672,3 +674,3 @@ t.tearDown(client.close.bind(client))

port: server.address().port,
protocol: 'http'
protocol: 'http:'
})

@@ -675,0 +677,0 @@ t.tearDown(client.destroy.bind(client))

@@ -18,3 +18,3 @@ 'use strict'

t.plan(clients * 3)
t.plan(clients * 6)

@@ -40,4 +40,7 @@ const server = createServer((req, res) => {

})
pool.on('disconnect', (client) => {
t.strictEqual(client instanceof Client, true)
pool.on('disconnect', (client, error) => {
t.true(client instanceof Client)
t.true(error instanceof errors.InformationalError)
t.strictEqual(error.code, 'UND_ERR_INFO')
t.strictEqual(error.message, 'socket idle timeout')
})

@@ -58,3 +61,3 @@

test('basic get', (t) => {
t.plan(9)
t.plan(14)

@@ -73,2 +76,4 @@ const server = createServer((req, res) => {

t.strictEqual(client.url.origin, `http://localhost:${server.address().port}`)
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {

@@ -87,4 +92,7 @@ t.error(err)

t.strictEqual(client.destroyed, false)
t.strictEqual(client.closed, false)
client.close((err) => {
t.error(err)
t.strictEqual(client.destroyed, true)
client.destroy((err) => {

@@ -97,4 +105,6 @@ t.error(err)

})
t.strictEqual(client.closed, true)
})
})
test('URL as arg', (t) => {

@@ -141,2 +151,3 @@ t.plan(9)

})
test('basic get error async/await', (t) => {

@@ -340,3 +351,3 @@ t.plan(2)

test('busy', (t) => {
t.plan(8 * 6)
t.plan(8 * 8 + 2 + 1)

@@ -356,5 +367,13 @@ const server = createServer((req, res) => {

})
let connected = 0
client.on('drain', () => {
t.pass()
})
client.on('connect', () => {
t.strictEqual(client.connected, ++connected)
})
t.tearDown(client.destroy.bind(client))
for (let n = 0; n < 8; ++n) {
for (let n = 1; n <= 8; ++n) {
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {

@@ -372,2 +391,4 @@ t.error(err)

})
t.strictEqual(client.pending, Math.max(n - 2, 0))
t.strictEqual(client.busy, n > 2)
}

@@ -986,3 +1007,3 @@ })

test('pool destroy fails queued requests', (t) => {
t.plan(4)
t.plan(6)

@@ -1016,5 +1037,7 @@ const server = createServer((req, res) => {

t.strictEqual(client.destroyed, false)
client.destroy(_err, () => {
t.pass()
})
t.strictEqual(client.destroyed, true)

@@ -1021,0 +1044,0 @@ client.request({

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