deps: update undici to 5.1.1

PR-URL: https://github.com/nodejs/node/pull/42939
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com>
This commit is contained in:
Michaël Zasso 2022-05-02 14:15:50 +02:00 committed by GitHub
parent 1d8a320a04
commit 3bd87e1782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2276 additions and 3570 deletions

View File

@ -2,7 +2,7 @@
[![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) [![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)
A HTTP/1.1 client, written from scratch for Node.js. An HTTP/1.1 client, written from scratch for Node.js.
> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici. > Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
It is also a Stranger Things reference. It is also a Stranger Things reference.
@ -65,7 +65,15 @@ for await (const data of body) {
console.log('trailers', trailers) console.log('trailers', trailers)
``` ```
Using [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). ## Body Mixins
The `body` mixins are the most common way to format the request/response body. Mixins include:
- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
Example usage:
```js ```js
import { request } from 'undici' import { request } from 'undici'
@ -83,6 +91,12 @@ console.log('data', await body.json())
console.log('trailers', trailers) console.log('trailers', trailers)
``` ```
_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._
Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format.
For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
## Common API Methods ## Common API Methods
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. 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.
@ -213,7 +227,7 @@ const data = {
#### `response.body` #### `response.body`
Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html) which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`. Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
```js ```js
import {fetch} from 'undici'; import {fetch} from 'undici';
@ -228,7 +242,7 @@ Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v1
#### Specification Compliance #### Specification Compliance
This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) which Undici does This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
not support or does not fully implement. not support or does not fully implement.
##### Garbage Collection ##### Garbage Collection
@ -239,7 +253,7 @@ The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consumi
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body. [garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
Garbage collection in Node is less aggressive and deterministic Garbage collection in Node is less aggressive and deterministic
(due to the lack of clear idle periods that browser have through the rendering refresh rate) (due to the lack of clear idle periods that browsers have through the rendering refresh rate)
which means that leaving the release of connection resources to the garbage collector can lead which means that leaving the release of connection resources to the garbage collector can lead
to excessive connection usage, reduced performance (due to less connection re-use), and even to excessive connection usage, reduced performance (due to less connection re-use), and even
stalls or deadlocks when running out of connections. stalls or deadlocks when running out of connections.
@ -301,7 +315,7 @@ Returns: `Dispatcher`
## Specification Compliance ## Specification Compliance
This section documents parts of the HTTP/1.1 specification which Undici does This section documents parts of the HTTP/1.1 specification that Undici does
not support or does not fully implement. not support or does not fully implement.
### Expect ### Expect
@ -334,7 +348,7 @@ aborted.
### Manual Redirect ### Manual Redirect
Since it is not possible to manually follow an HTTP redirect on server-side, Since it is not possible to manually follow an HTTP redirect on the server-side,
Undici returns the actual response instead of an `opaqueredirect` filtered one Undici returns the actual response instead of an `opaqueredirect` filtered one
when invoked with a `manual` redirect. This aligns `fetch()` with the other when invoked with a `manual` redirect. This aligns `fetch()` with the other
implementations in Deno and Cloudflare Workers. implementations in Deno and Cloudflare Workers.

View File

@ -193,7 +193,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
* **path** `string` * **path** `string`
* **method** `string` * **method** `string`
* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
* **headers** `UndiciHeaders` (optional) - Default: `null` * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
* **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - 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. * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - 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.
* **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received. * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.

View File

@ -445,3 +445,79 @@ mockAgent.disableNetConnect()
await request('http://example.com') await request('http://example.com')
// Will throw // Will throw
``` ```
### `MockAgent.pendingInterceptors()`
This method returns any pending interceptors registered on a mock agent. A pending interceptor meets one of the following criteria:
- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
Returns: `PendingInterceptor[]` (where `PendingInterceptor` is a `MockDispatch` with an additional `origin: string`)
#### Example - List all pending inteceptors
```js
const agent = new MockAgent()
agent.disableNetConnect()
agent
.get('https://example.com')
.intercept({ method: 'GET', path: '/' })
.reply(200, '')
const pendingInterceptors = agent.pendingInterceptors()
// Returns [
// {
// timesInvoked: 0,
// times: 1,
// persist: false,
// consumed: false,
// pending: true,
// path: '/',
// method: 'GET',
// body: undefined,
// headers: undefined,
// data: {
// error: null,
// statusCode: 200,
// data: '',
// headers: {},
// trailers: {}
// },
// origin: 'https://example.com'
// }
// ]
```
### `MockAgent.assertNoPendingInterceptors([options])`
This method throws if the mock agent has any pending interceptors. A pending interceptor meets one of the following criteria:
- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
#### Example - Check that there are no pending interceptors
```js
const agent = new MockAgent()
agent.disableNetConnect()
agent
.get('https://example.com')
.intercept({ method: 'GET', path: '/' })
.reply(200, '')
agent.assertNoPendingInterceptors()
// Throws an UndiciError with the following message:
//
// 1 interceptor is pending:
//
// ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
// │ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
// ├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
// │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │
// └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
```

View File

@ -5,17 +5,20 @@ Undici have its own mocking [utility](../api/MockAgent.md). It allow us to inter
Example: Example:
```js ```js
// index.mjs // bank.mjs
import { request } from 'undici' import { request } from 'undici'
export async function bankTransfer(recepient, ammount) { export async function bankTransfer(recepient, amount) {
const { body } = await request('http://localhost:3000/bank-transfer', const { body } = await request('http://localhost:3000/bank-transfer',
{ {
method: 'POST', method: 'POST',
headers: { headers: {
'X-TOKEN-SECRET': 'SuperSecretToken', 'X-TOKEN-SECRET': 'SuperSecretToken',
}, },
body: JSON.stringify({ recepient }) body: JSON.stringify({
recepient,
amount
})
} }
) )
return await body.json() return await body.json()
@ -28,7 +31,7 @@ And this is what the test file looks like:
// index.test.mjs // index.test.mjs
import { strict as assert } from 'assert' import { strict as assert } from 'assert'
import { MockAgent, setGlobalDispatcher, } from 'undici' import { MockAgent, setGlobalDispatcher, } from 'undici'
import { bankTransfer } from './undici.mjs' import { bankTransfer } from './bank.mjs'
const mockAgent = new MockAgent(); const mockAgent = new MockAgent();
@ -46,7 +49,7 @@ mockPool.intercept({
}, },
body: JSON.stringify({ body: JSON.stringify({
recepient: '1234567890', recepient: '1234567890',
ammount: '100' amount: '100'
}) })
}).reply(200, { }).reply(200, {
message: 'transaction processed' message: 'transaction processed'
@ -94,7 +97,7 @@ mockPool.intercept({
const badRequest = await bankTransfer('1234567890', '100') const badRequest = await bankTransfer('1234567890', '100')
// Will throw an error // Will throw an error
// MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer': // MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer':
// subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled) // subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
``` ```

14
deps/undici/src/index-fetch.js vendored Normal file
View File

@ -0,0 +1,14 @@
'use strict'
const Agent = require('./lib/agent')
const globalDispatcher = new Agent()
const fetchImpl = require('./lib/fetch')
module.exports.fetch = async function fetch (resource) {
return fetchImpl.apply(globalDispatcher, arguments)
}
module.exports.FormData = require('./lib/fetch/formdata').FormData
module.exports.Headers = require('./lib/fetch/headers').Headers
module.exports.Response = require('./lib/fetch/response').Response
module.exports.Request = require('./lib/fetch/request').Request

View File

@ -16,6 +16,7 @@ import { request, pipeline, stream, connect, upgrade } from './types/api'
export * from './types/fetch' export * from './types/fetch'
export * from './types/file' export * from './types/file'
export * from './types/formdata' export * from './types/formdata'
export { Interceptable } from './types/mock-interceptor'
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent } export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
export default Undici export default Undici

View File

@ -88,14 +88,16 @@ class RequestHandler extends AsyncResource {
this.res = body this.res = body
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
this.runInAsyncScope(callback, null, null, { if (callback !== null) {
statusCode, this.runInAsyncScope(callback, null, null, {
headers, statusCode,
trailers: this.trailers, headers,
opaque, trailers: this.trailers,
body, opaque,
context body,
}) context
})
}
} }
onData (chunk) { onData (chunk) {

View File

@ -11,6 +11,12 @@ const kHandler = Symbol('handler')
const channels = {} const channels = {}
let extractBody
const nodeVersion = process.versions.node.split('.')
const nodeMajor = Number(nodeVersion[0])
const nodeMinor = Number(nodeVersion[1])
try { try {
const diagnosticsChannel = require('diagnostics_channel') const diagnosticsChannel = require('diagnostics_channel')
channels.create = diagnosticsChannel.channel('undici:request:create') channels.create = diagnosticsChannel.channel('undici:request:create')
@ -79,7 +85,7 @@ class Request {
this.body = body.byteLength ? body : null this.body = body.byteLength ? body : null
} else if (typeof body === 'string') { } else if (typeof body === 'string') {
this.body = body.length ? Buffer.from(body) : null this.body = body.length ? Buffer.from(body) : null
} else if (util.isIterable(body) || util.isBlobLike(body)) { } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
this.body = body this.body = body
} else { } else {
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
@ -126,7 +132,22 @@ class Request {
throw new InvalidArgumentError('headers must be an object or an array') throw new InvalidArgumentError('headers must be an object or an array')
} }
if (util.isBlobLike(body) && this.contentType == null && body.type) { if (util.isFormDataLike(this.body)) {
if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 5)) {
throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.5 and newer.')
}
if (!extractBody) {
extractBody = require('../fetch/body.js').extractBody
}
const [bodyStream, contentType] = extractBody(body)
if (this.contentType == null) {
this.contentType = contentType
this.headers += `content-type: ${contentType}\r\n`
}
this.body = bodyStream.stream
} else if (util.isBlobLike(body) && this.contentType == null && body.type) {
this.contentType = body.type this.contentType = body.type
this.headers += `content-type: ${body.type}\r\n` this.headers += `content-type: ${body.type}\r\n`
} }

View File

@ -324,6 +324,10 @@ function ReadableStreamFrom (iterable) {
) )
} }
function isFormDataLike (chunk) {
return chunk && chunk.constructor && chunk.constructor.name === 'FormData'
}
const kEnumerableProperty = Object.create(null) const kEnumerableProperty = Object.create(null)
kEnumerableProperty.enumerable = true kEnumerableProperty.enumerable = true
@ -352,5 +356,6 @@ module.exports = {
ReadableStreamFrom, ReadableStreamFrom,
isBuffer, isBuffer,
validateHandler, validateHandler,
getSocketInfo getSocketInfo,
isFormDataLike
} }

View File

@ -71,7 +71,7 @@ function extractBody (object, keepalive = false) {
// Set source to a copy of the bytes held by object. // Set source to a copy of the bytes held by object.
source = new Uint8Array(object) source = new Uint8Array(object)
} else if (object instanceof FormData) { } else if (util.isFormDataLike(object)) {
const boundary = '----formdata-undici-' + Math.random() const boundary = '----formdata-undici-' + Math.random()
const prefix = `--${boundary}\r\nContent-Disposition: form-data` const prefix = `--${boundary}\r\nContent-Disposition: form-data`
@ -348,7 +348,7 @@ const properties = {
bodyUsed: { bodyUsed: {
enumerable: true, enumerable: true,
get () { get () {
return this[kState].body && util.isDisturbed(this[kState].body.stream) return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
} }
} }
} }

View File

@ -11,22 +11,8 @@ const {
forbiddenResponseHeaderNames forbiddenResponseHeaderNames
} = require('./constants') } = require('./constants')
function binarySearch (arr, val) { const kHeadersMap = Symbol('headers map')
let low = 0 const kHeadersSortedMap = Symbol('headers map sorted')
let high = Math.floor(arr.length / 2)
while (high > low) {
const mid = (high + low) >>> 1
if (val.localeCompare(arr[mid * 2]) > 0) {
low = mid + 1
} else {
high = mid
}
}
return low * 2
}
function normalizeAndValidateHeaderName (name) { function normalizeAndValidateHeaderName (name) {
if (name === undefined) { if (name === undefined) {
@ -91,64 +77,74 @@ function fill (headers, object) {
} }
} }
// TODO: Composition over inheritence? Or helper methods? class HeadersList {
class HeadersList extends Array { constructor (init) {
append (name, value) { if (init instanceof HeadersList) {
const normalizedName = normalizeAndValidateHeaderName(name) this[kHeadersMap] = new Map(init[kHeadersMap])
const normalizedValue = normalizeAndValidateHeaderValue(name, value) this[kHeadersSortedMap] = init[kHeadersSortedMap]
const index = binarySearch(this, normalizedName)
if (this[index] === normalizedName) {
this[index + 1] += `, ${normalizedValue}`
} else { } else {
this.splice(index, 0, normalizedName, normalizedValue) this[kHeadersMap] = new Map(init)
this[kHeadersSortedMap] = null
} }
} }
delete (name) { append (name, value) {
this[kHeadersSortedMap] = null
const normalizedName = normalizeAndValidateHeaderName(name) const normalizedName = normalizeAndValidateHeaderName(name)
const normalizedValue = normalizeAndValidateHeaderValue(name, value)
const index = binarySearch(this, normalizedName) const exists = this[kHeadersMap].get(normalizedName)
if (this[index] === normalizedName) { if (exists) {
this.splice(index, 2) this[kHeadersMap].set(normalizedName, `${exists}, ${normalizedValue}`)
} else {
this[kHeadersMap].set(normalizedName, `${normalizedValue}`)
} }
} }
set (name, value) {
this[kHeadersSortedMap] = null
const normalizedName = normalizeAndValidateHeaderName(name)
return this[kHeadersMap].set(normalizedName, value)
}
delete (name) {
this[kHeadersSortedMap] = null
const normalizedName = normalizeAndValidateHeaderName(name)
return this[kHeadersMap].delete(normalizedName)
}
get (name) { get (name) {
const normalizedName = normalizeAndValidateHeaderName(name) const normalizedName = normalizeAndValidateHeaderName(name)
return this[kHeadersMap].get(normalizedName) ?? null
const index = binarySearch(this, normalizedName)
if (this[index] === normalizedName) {
return this[index + 1]
}
return null
} }
has (name) { has (name) {
const normalizedName = normalizeAndValidateHeaderName(name) const normalizedName = normalizeAndValidateHeaderName(name)
return this[kHeadersMap].has(normalizedName)
const index = binarySearch(this, normalizedName)
return this[index] === normalizedName
} }
set (name, value) { keys () {
const normalizedName = normalizeAndValidateHeaderName(name) return this[kHeadersMap].keys()
const normalizedValue = normalizeAndValidateHeaderValue(name, value) }
const index = binarySearch(this, normalizedName) values () {
if (this[index] === normalizedName) { return this[kHeadersMap].values()
this[index + 1] = normalizedValue }
} else {
this.splice(index, 0, normalizedName, normalizedValue) entries () {
} return this[kHeadersMap].entries()
}
[Symbol.iterator] () {
return this[kHeadersMap][Symbol.iterator]()
} }
} }
// https://fetch.spec.whatwg.org/#headers-class
class Headers { class Headers {
constructor (...args) { constructor (...args) {
if ( if (
@ -161,7 +157,6 @@ class Headers {
) )
} }
const init = args.length >= 1 ? args[0] ?? {} : {} const init = args.length >= 1 ? args[0] ?? {} : {}
this[kHeadersList] = new HeadersList() this[kHeadersList] = new HeadersList()
// The new Headers(init) constructor steps are: // The new Headers(init) constructor steps are:
@ -287,20 +282,18 @@ class Headers {
) )
} }
const normalizedName = normalizeAndValidateHeaderName(String(args[0]))
if (this[kGuard] === 'immutable') { if (this[kGuard] === 'immutable') {
throw new TypeError('immutable') throw new TypeError('immutable')
} else if ( } else if (
this[kGuard] === 'request' && this[kGuard] === 'request' &&
forbiddenHeaderNames.includes(normalizedName) forbiddenHeaderNames.includes(String(args[0]).toLocaleLowerCase())
) { ) {
return return
} else if (this[kGuard] === 'request-no-cors') { } else if (this[kGuard] === 'request-no-cors') {
// TODO // TODO
} else if ( } else if (
this[kGuard] === 'response' && this[kGuard] === 'response' &&
forbiddenResponseHeaderNames.includes(normalizedName) forbiddenResponseHeaderNames.includes(String(args[0]).toLocaleLowerCase())
) { ) {
return return
} }
@ -308,25 +301,41 @@ class Headers {
return this[kHeadersList].set(String(args[0]), String(args[1])) return this[kHeadersList].set(String(args[0]), String(args[1]))
} }
* keys () { get [kHeadersSortedMap] () {
const clone = this[kHeadersList].slice() this[kHeadersList][kHeadersSortedMap] ??= new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1))
for (let index = 0; index < clone.length; index += 2) { return this[kHeadersList][kHeadersSortedMap]
yield clone[index]
}
} }
* values () { keys () {
const clone = this[kHeadersList].slice() if (!(this instanceof Headers)) {
for (let index = 1; index < clone.length; index += 2) { throw new TypeError('Illegal invocation')
yield clone[index]
} }
return this[kHeadersSortedMap].keys()
} }
* entries () { values () {
const clone = this[kHeadersList].slice() if (!(this instanceof Headers)) {
for (let index = 0; index < clone.length; index += 2) { throw new TypeError('Illegal invocation')
yield [clone[index], clone[index + 1]]
} }
return this[kHeadersSortedMap].values()
}
entries () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
return this[kHeadersSortedMap].entries()
}
[Symbol.iterator] () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
return this[kHeadersSortedMap]
} }
forEach (...args) { forEach (...args) {
@ -346,15 +355,9 @@ class Headers {
const callback = args[0] const callback = args[0]
const thisArg = args[1] const thisArg = args[1]
const clone = this[kHeadersList].slice() this[kHeadersSortedMap].forEach((value, index) => {
for (let index = 0; index < clone.length; index += 2) { callback.apply(thisArg, [value, index, this])
callback.call( })
thisArg,
clone[index + 1],
clone[index],
this
)
}
} }
[Symbol.for('nodejs.util.inspect.custom')] () { [Symbol.for('nodejs.util.inspect.custom')] () {
@ -384,7 +387,6 @@ module.exports = {
fill, fill,
Headers, Headers,
HeadersList, HeadersList,
binarySearch,
normalizeAndValidateHeaderName, normalizeAndValidateHeaderName,
normalizeAndValidateHeaderValue normalizeAndValidateHeaderValue
} }

View File

@ -768,7 +768,7 @@ async function schemeFetch (fetchParams) {
const { const {
protocol: scheme, protocol: scheme,
pathname: path pathname: path
} = new URL(requestCurrentURL(request)) } = requestCurrentURL(request)
// switch on requests current URLs scheme, and run the associated steps: // switch on requests current URLs scheme, and run the associated steps:
switch (scheme) { switch (scheme) {
@ -780,7 +780,7 @@ async function schemeFetch (fetchParams) {
const resp = makeResponse({ const resp = makeResponse({
statusText: 'OK', statusText: 'OK',
headersList: [ headersList: [
'content-type', 'text/html;charset=utf-8' ['content-type', 'text/html;charset=utf-8']
] ]
}) })
@ -792,7 +792,7 @@ async function schemeFetch (fetchParams) {
return makeNetworkError('invalid path called') return makeNetworkError('invalid path called')
} }
case 'blob:': { case 'blob:': {
resolveObjectURL ??= require('buffer').resolveObjectURL resolveObjectURL = resolveObjectURL || require('buffer').resolveObjectURL
// 1. Run these steps, but abort when the ongoing fetch is terminated: // 1. Run these steps, but abort when the ongoing fetch is terminated:
// 1. Let blob be requests current URLs blob URL entrys object. // 1. Let blob be requests current URLs blob URL entrys object.
@ -871,7 +871,7 @@ async function schemeFetch (fetchParams) {
return makeResponse({ return makeResponse({
statusText: 'OK', statusText: 'OK',
headersList: [ headersList: [
'content-type', contentType ['content-type', contentType]
], ],
body: extractBody(dataURLStruct.body)[0] body: extractBody(dataURLStruct.body)[0]
}) })
@ -1919,8 +1919,10 @@ async function httpNetworkFetch (
origin: url.origin, origin: url.origin,
method: request.method, method: request.method,
body: fetchParams.controller.dispatcher[kIsMockActive] ? request.body && request.body.source : body, body: fetchParams.controller.dispatcher[kIsMockActive] ? request.body && request.body.source : body,
headers: request.headersList, headers: [...request.headersList].flat(),
maxRedirections: 0 maxRedirections: 0,
bodyTimeout: 300_000,
headersTimeout: 300_000
}, },
{ {
body: null, body: null,
@ -1962,16 +1964,18 @@ async function httpNetworkFetch (
const decoders = [] const decoders = []
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
for (const coding of codings) { if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) {
if (/(x-)?gzip/.test(coding)) { for (const coding of codings) {
decoders.push(zlib.createGunzip()) if (/(x-)?gzip/.test(coding)) {
} else if (/(x-)?deflate/.test(coding)) { decoders.push(zlib.createGunzip())
decoders.push(zlib.createInflate()) } else if (/(x-)?deflate/.test(coding)) {
} else if (coding === 'br') { decoders.push(zlib.createInflate())
decoders.push(zlib.createBrotliDecompress()) } else if (coding === 'br') {
} else { decoders.push(zlib.createBrotliDecompress())
decoders.length = 0 } else {
break decoders.length = 0
break
}
} }
} }
@ -2029,7 +2033,7 @@ async function httpNetworkFetch (
fetchParams.controller.terminate(error) fetchParams.controller.terminate(error)
reject(makeNetworkError(error)) reject(error)
} }
} }
)) ))

View File

@ -420,7 +420,15 @@ class Request {
// 4. If headers is a Headers object, then for each header in its header // 4. If headers is a Headers object, then for each header in its header
// list, append headers name/headers value to thiss headers. // list, append headers name/headers value to thiss headers.
if (headers instanceof Headers) { if (headers instanceof Headers) {
this[kState].headersList.push(...headers[kHeadersList]) // TODO (fix): Why doesn't this work?
// for (const [key, val] of headers[kHeadersList]) {
// this[kHeaders].append(key, val)
// }
this[kState].headersList = new HeadersList([
...this[kState].headersList,
...headers[kHeadersList]
])
} else { } else {
// 5. Otherwise, fill thiss headers with headers. // 5. Otherwise, fill thiss headers with headers.
fillHeaders(this[kState].headersList, headers) fillHeaders(this[kState].headersList, headers)
@ -460,6 +468,7 @@ class Request {
// thiss headers. // thiss headers.
if (contentType && !this[kHeaders].has('content-type')) { if (contentType && !this[kHeaders].has('content-type')) {
this[kHeaders].append('content-type', contentType) this[kHeaders].append('content-type', contentType)
this[kState].headersList.append('content-type', contentType)
} }
} }
@ -793,9 +802,8 @@ function makeRequest (init) {
timingAllowFailed: false, timingAllowFailed: false,
...init, ...init,
headersList: init.headersList headersList: init.headersList
? new HeadersList(...init.headersList) ? new HeadersList(init.headersList)
: new HeadersList(), : new HeadersList()
urlList: init.urlList ? [...init.urlList.map((url) => new URL(url))] : []
} }
request.url = request.urlList[0] request.url = request.urlList[0]
return request return request

View File

@ -81,7 +81,7 @@ class Response {
const value = parsedURL.toString() const value = parsedURL.toString()
// 7. Append `Location`/value to responseObjects responses header list. // 7. Append `Location`/value to responseObjects responses header list.
responseObject[kState].headersList.push('location', value) responseObject[kState].headersList.append('location', value)
// 8. Return responseObject. // 8. Return responseObject.
return responseObject return responseObject
@ -172,7 +172,7 @@ class Response {
// not contain `Content-Type`, then append `Content-Type`/Content-Type // not contain `Content-Type`, then append `Content-Type`/Content-Type
// to thiss responses header list. // to thiss responses header list.
if (contentType && !this.headers.has('content-type')) { if (contentType && !this.headers.has('content-type')) {
this.headers.set('content-type', contentType) this.headers.append('content-type', contentType)
} }
} }
} }
@ -350,7 +350,7 @@ function makeResponse (init) {
statusText: '', statusText: '',
...init, ...init,
headersList: init.headersList headersList: init.headersList
? new HeadersList(...init.headersList) ? new HeadersList(init.headersList)
: new HeadersList(), : new HeadersList(),
urlList: init.urlList ? [...init.urlList] : [] urlList: init.urlList ? [...init.urlList] : []
} }
@ -393,17 +393,15 @@ function makeFilteredHeadersList (headersList, filter) {
get (target, prop) { get (target, prop) {
// Override methods used by Headers class. // Override methods used by Headers class.
if (prop === 'get' || prop === 'has') { if (prop === 'get' || prop === 'has') {
return (name) => filter(name) ? target[prop](name) : undefined const defaultReturn = prop === 'has' ? false : null
} else if (prop === 'slice') { return (name) => filter(name) ? target[prop](name) : defaultReturn
return (...args) => { } else if (prop === Symbol.iterator) {
assert(args.length === 0) return function * () {
const arr = [] for (const entry of target) {
for (let index = 0; index < target.length; index += 2) { if (filter(entry[0])) {
if (filter(target[index])) { yield entry
arr.push(target[index], target[index + 1])
} }
} }
return arr
} }
} else { } else {
return target[prop] return target[prop]
@ -423,7 +421,10 @@ function filterResponse (response, type) {
return makeFilteredResponse(response, { return makeFilteredResponse(response, {
type: 'basic', type: 'basic',
headersList: makeFilteredHeadersList(response.headersList, (name) => !forbiddenResponseHeaderNames.includes(name)) headersList: makeFilteredHeadersList(
response.headersList,
(name) => !forbiddenResponseHeaderNames.includes(name.toLowerCase())
)
}) })
} else if (type === 'cors') { } else if (type === 'cors') {
// A CORS filtered response is a filtered response whose type is "cors" // A CORS filtered response is a filtered response whose type is "cors"

View File

@ -318,7 +318,42 @@ function sameOrigin (A, B) {
// https://fetch.spec.whatwg.org/#corb-check // https://fetch.spec.whatwg.org/#corb-check
function CORBCheck (request, response) { function CORBCheck (request, response) {
// TODO // 1. If requests initiator is "download", then return allowed.
if (request.initiator === 'download') {
return 'allowed'
}
// 2. If requests current URLs scheme is not an HTTP(S) scheme, then return allowed.
if (!/^https?$/.test(request.currentURL.scheme)) {
return 'allowed'
}
// 3. Let mimeType be the result of extracting a MIME type from responses header list.
const mimeType = response.headersList.get('content-type')
// 4. If mimeType is failure, then return allowed.
if (mimeType === '') {
return 'allowed'
}
// 5. If responses status is 206 and mimeType is a CORB-protected MIME type, then return blocked.
const isCORBProtectedMIME =
(/^text\/html\b/.test(mimeType) ||
/^application\/javascript\b/.test(mimeType) ||
/^application\/xml\b/.test(mimeType)) && !/^application\/xml\+svg\b/.test(mimeType)
if (response.status === 206 && isCORBProtectedMIME) {
return 'blocked'
}
// 6. If determine nosniff with responses header list is true and mimeType is a CORB-protected MIME type or its essence is "text/plain", then return blocked.
// https://fetch.spec.whatwg.org/#determinenosniff
if (response.headersList.get('x-content-type-options') && isCORBProtectedMIME) {
return 'blocked'
}
// 7. Return allowed.
return 'allowed' return 'allowed'
} }

View File

@ -16,8 +16,10 @@ const {
const MockClient = require('./mock-client') const MockClient = require('./mock-client')
const MockPool = require('./mock-pool') const MockPool = require('./mock-pool')
const { matchValue, buildMockOptions } = require('./mock-utils') const { matchValue, buildMockOptions } = require('./mock-utils')
const { InvalidArgumentError } = require('../core/errors') const { InvalidArgumentError, UndiciError } = require('../core/errors')
const Dispatcher = require('../dispatcher') const Dispatcher = require('../dispatcher')
const Pluralizer = require('./pluralizer')
const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
class FakeWeakRef { class FakeWeakRef {
constructor (value) { constructor (value) {
@ -134,6 +136,30 @@ class MockAgent extends Dispatcher {
[kGetNetConnect] () { [kGetNetConnect] () {
return this[kNetConnect] return this[kNetConnect]
} }
pendingInterceptors () {
const mockAgentClients = this[kClients]
return Array.from(mockAgentClients.entries())
.flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin })))
.filter(({ pending }) => pending)
}
assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
const pending = this.pendingInterceptors()
if (pending.length === 0) {
return
}
const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length)
throw new UndiciError(`
${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending:
${pendingInterceptorsFormatter.format(pending)}
`.trim())
}
} }
module.exports = MockAgent module.exports = MockAgent

View File

@ -12,7 +12,7 @@ const {
const { InvalidArgumentError } = require('../core/errors') const { InvalidArgumentError } = require('../core/errors')
/** /**
* Defines the scope API for a interceptor reply * Defines the scope API for an interceptor reply
*/ */
class MockScope { class MockScope {
constructor (mockDispatch) { constructor (mockDispatch) {
@ -74,6 +74,9 @@ class MockInterceptor {
const parsedURL = new URL(opts.path, 'data://') const parsedURL = new URL(opts.path, 'data://')
opts.path = parsedURL.pathname + parsedURL.search opts.path = parsedURL.pathname + parsedURL.search
} }
if (typeof opts.method === 'string') {
opts.method = opts.method.toUpperCase()
}
this[kDispatchKey] = buildKey(opts) this[kDispatchKey] = buildKey(opts)
this[kDispatches] = mockDispatches this[kDispatches] = mockDispatches

View File

@ -31,6 +31,26 @@ function lowerCaseEntries (headers) {
) )
} }
/**
* @param {import('../../index').Headers|string[]|Record<string, string>} headers
* @param {string} key
*/
function getHeaderByName (headers, key) {
if (Array.isArray(headers)) {
for (let i = 0; i < headers.length; i += 2) {
if (headers[i] === key) {
return headers[i + 1]
}
}
return undefined
} else if (typeof headers.get === 'function') {
return headers.get(key)
} else {
return headers[key]
}
}
function matchHeaders (mockDispatch, headers) { function matchHeaders (mockDispatch, headers) {
if (typeof mockDispatch.headers === 'function') { if (typeof mockDispatch.headers === 'function') {
if (Array.isArray(headers)) { // fetch HeadersList if (Array.isArray(headers)) { // fetch HeadersList
@ -51,9 +71,9 @@ function matchHeaders (mockDispatch, headers) {
} }
for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
const header = typeof headers.get === 'function' ? headers.get(matchHeaderName) : headers[matchHeaderName] const headerValue = getHeaderByName(headers, matchHeaderName)
if (!matchValue(matchHeaderValue, header)) { if (!matchValue(matchHeaderValue, headerValue)) {
return false return false
} }
} }
@ -107,9 +127,9 @@ function getMockDispatch (mockDispatches, key) {
} }
function addMockDispatch (mockDispatches, key, data) { function addMockDispatch (mockDispatches, key, data) {
const baseData = { times: null, persist: false, consumed: false } const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false }
const replyData = typeof data === 'function' ? { callback: data } : { ...data } const replyData = typeof data === 'function' ? { callback: data } : { ...data }
const newMockDispatch = { ...baseData, ...key, data: { error: null, ...replyData } } const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
mockDispatches.push(newMockDispatch) mockDispatches.push(newMockDispatch)
return newMockDispatch return newMockDispatch
} }
@ -140,6 +160,80 @@ function generateKeyValues (data) {
return Object.entries(data).reduce((keyValuePairs, [key, value]) => [...keyValuePairs, key, value], []) return Object.entries(data).reduce((keyValuePairs, [key, value]) => [...keyValuePairs, key, value], [])
} }
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {number} statusCode
*/
function getStatusText (statusCode) {
switch (statusCode) {
case 100: return 'Continue'
case 101: return 'Switching Protocols'
case 102: return 'Processing'
case 103: return 'Early Hints'
case 200: return 'OK'
case 201: return 'Created'
case 202: return 'Accepted'
case 203: return 'Non-Authoritative Information'
case 204: return 'No Content'
case 205: return 'Reset Content'
case 206: return 'Partial Content'
case 207: return 'Multi-Status'
case 208: return 'Already Reported'
case 226: return 'IM Used'
case 300: return 'Multiple Choice'
case 301: return 'Moved Permanently'
case 302: return 'Found'
case 303: return 'See Other'
case 304: return 'Not Modified'
case 305: return 'Use Proxy'
case 306: return 'unused'
case 307: return 'Temporary Redirect'
case 308: return 'Permanent Redirect'
case 400: return 'Bad Request'
case 401: return 'Unauthorized'
case 402: return 'Payment Required'
case 403: return 'Forbidden'
case 404: return 'Not Found'
case 405: return 'Method Not Allowed'
case 406: return 'Not Acceptable'
case 407: return 'Proxy Authentication Required'
case 408: return 'Request Timeout'
case 409: return 'Conflict'
case 410: return 'Gone'
case 411: return 'Length Required'
case 412: return 'Precondition Failed'
case 413: return 'Payload Too Large'
case 414: return 'URI Too Large'
case 415: return 'Unsupported Media Type'
case 416: return 'Range Not Satisfiable'
case 417: return 'Expectation Failed'
case 418: return 'I\'m a teapot'
case 421: return 'Misdirected Request'
case 422: return 'Unprocessable Entity'
case 423: return 'Locked'
case 424: return 'Failed Dependency'
case 425: return 'Too Early'
case 426: return 'Upgrade Required'
case 428: return 'Precondition Required'
case 429: return 'Too Many Requests'
case 431: return 'Request Header Fields Too Large'
case 451: return 'Unavailable For Legal Reasons'
case 500: return 'Internal Server Error'
case 501: return 'Not Implemented'
case 502: return 'Bad Gateway'
case 503: return 'Service Unavailable'
case 504: return 'Gateway Timeout'
case 505: return 'HTTP Version Not Supported'
case 506: return 'Variant Also Negotiates'
case 507: return 'Insufficient Storage'
case 508: return 'Loop Detected'
case 510: return 'Not Extended'
case 511: return 'Network Authentication Required'
default:
throw new ReferenceError(`Unknown status code "${statusCode}"!`)
}
}
async function getResponse (body) { async function getResponse (body) {
const buffers = [] const buffers = []
for await (const data of body) { for await (const data of body) {
@ -156,6 +250,8 @@ function mockDispatch (opts, handler) {
const key = buildKey(opts) const key = buildKey(opts)
const mockDispatch = getMockDispatch(this[kDispatches], key) const mockDispatch = getMockDispatch(this[kDispatches], key)
mockDispatch.timesInvoked++
// Here's where we resolve a callback if a callback is present for the dispatch data. // Here's where we resolve a callback if a callback is present for the dispatch data.
if (mockDispatch.data.callback) { if (mockDispatch.data.callback) {
mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) }
@ -163,18 +259,11 @@ function mockDispatch (opts, handler) {
// Parse mockDispatch data // Parse mockDispatch data
const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch
let { times } = mockDispatch const { timesInvoked, times } = mockDispatch
if (typeof times === 'number' && times > 0) {
times = --mockDispatch.times
}
// If persist is true, skip // If it's used up and not persistent, mark as consumed
// Or if times is a number and > 0, skip mockDispatch.consumed = !persist && timesInvoked >= times
// Otherwise, mark as consumed mockDispatch.pending = timesInvoked < times
if (!(persist === true || (typeof times === 'number' && times > 0))) {
mockDispatch.consumed = true
}
// If specified, trigger dispatch error // If specified, trigger dispatch error
if (error !== null) { if (error !== null) {
@ -197,7 +286,7 @@ function mockDispatch (opts, handler) {
const responseHeaders = generateKeyValues(headers) const responseHeaders = generateKeyValues(headers)
const responseTrailers = generateKeyValues(trailers) const responseTrailers = generateKeyValues(trailers)
handler.onHeaders(statusCode, responseHeaders, resume) handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
handler.onData(Buffer.from(responseData)) handler.onData(Buffer.from(responseData))
handler.onComplete(responseTrailers) handler.onComplete(responseTrailers)
deleteMockDispatch(mockDispatches, key) deleteMockDispatch(mockDispatches, key)
@ -264,8 +353,10 @@ module.exports = {
generateKeyValues, generateKeyValues,
matchValue, matchValue,
getResponse, getResponse,
getStatusText,
mockDispatch, mockDispatch,
buildMockDispatch, buildMockDispatch,
checkNetConnect, checkNetConnect,
buildMockOptions buildMockOptions,
getHeaderByName
} }

View File

@ -0,0 +1,40 @@
'use strict'
const { Transform } = require('stream')
const { Console } = require('console')
/**
* Gets the output of `console.table(…)` as a string.
*/
module.exports = class PendingInterceptorsFormatter {
constructor ({ disableColors } = {}) {
this.transform = new Transform({
transform (chunk, _enc, cb) {
cb(null, chunk)
}
})
this.logger = new Console({
stdout: this.transform,
inspectOptions: {
colors: !disableColors && !process.env.CI
}
})
}
format (pendingInterceptors) {
const withPrettyHeaders = pendingInterceptors.map(
({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
Method: method,
Origin: origin,
Path: path,
'Status code': statusCode,
Persistent: persist ? '✅' : '❌',
Invocations: timesInvoked,
Remaining: persist ? Infinity : times - timesInvoked
}))
this.logger.table(withPrettyHeaders)
return this.transform.read().toString()
}
}

29
deps/undici/src/lib/mock/pluralizer.js vendored Normal file
View File

@ -0,0 +1,29 @@
'use strict'
const singulars = {
pronoun: 'it',
is: 'is',
was: 'was',
this: 'this'
}
const plurals = {
pronoun: 'they',
is: 'are',
was: 'were',
this: 'these'
}
module.exports = class Pluralizer {
constructor (singular, plural) {
this.singular = singular
this.plural = plural
}
pluralize (count) {
const one = count === 1
const keys = one ? singulars : plurals
const noun = one ? this.singular : this.plural
return { ...keys, count, noun }
}
}

View File

@ -23,7 +23,7 @@ class ProxyAgent extends DispatcherBase {
origin: this[kProxy].uri, origin: this[kProxy].uri,
path: opts.origin + opts.path, path: opts.origin + opts.path,
headers: { headers: {
...opts.headers, ...buildHeaders(opts.headers),
host host
} }
}, },
@ -55,4 +55,25 @@ function buildProxyOptions (opts) {
} }
} }
/**
* @param {string[] | Record<string, string>} headers
* @returns {Record<string, string>}
*/
function buildHeaders (headers) {
// When using undici.fetch, the headers list is stored
// as an array.
if (Array.isArray(headers)) {
/** @type {Record<string, string>} */
const headersPair = {}
for (let i = 0; i < headers.length; i += 2) {
headersPair[headers[i]] = headers[i + 1]
}
return headersPair
}
return headers
}
module.exports = ProxyAgent module.exports = ProxyAgent

View File

@ -1,6 +1,6 @@
{ {
"name": "undici", "name": "undici",
"version": "5.0.0", "version": "5.1.1",
"description": "An HTTP/1.1 client, written from scratch for Node.js", "description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org", "homepage": "https://undici.nodejs.org",
"bugs": { "bugs": {
@ -35,12 +35,13 @@
"files": [ "files": [
"*.d.ts", "*.d.ts",
"index.js", "index.js",
"index-fetch.js",
"lib", "lib",
"types", "types",
"docs" "docs"
], ],
"scripts": { "scripts": {
"build:node": "npx esbuild@0.14.25 index.js --bundle --platform=node --outfile=undici.js", "build:node": "npx esbuild@0.14.38 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js",
"prebuild:wasm": "docker build -t llhttp_wasm_builder -f build/Dockerfile .", "prebuild:wasm": "docker build -t llhttp_wasm_builder -f build/Dockerfile .",
"build:wasm": "node build/wasm.js --docker", "build:wasm": "node build/wasm.js --docker",
"lint": "standard | snazzy", "lint": "standard | snazzy",
@ -63,22 +64,22 @@
"fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus"
}, },
"devDependencies": { "devDependencies": {
"@sinonjs/fake-timers": "^7.0.5", "@sinonjs/fake-timers": "^9.1.2",
"@types/node": "^16.9.1", "@types/node": "^17.0.29",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"busboy": "^0.3.1", "busboy": "^0.3.1",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chai-iterator": "^3.0.2", "chai-iterator": "^3.0.2",
"chai-string": "^1.5.0", "chai-string": "^1.5.0",
"concurrently": "^6.2.1", "concurrently": "^7.1.0",
"cronometro": "^0.8.0", "cronometro": "^0.8.0",
"delay": "^5.0.0", "delay": "^5.0.0",
"docsify-cli": "^4.4.3", "docsify-cli": "^4.4.3",
"formdata-node": "^4.3.1", "formdata-node": "^4.3.1",
"https-pem": "^2.0.0", "https-pem": "^2.0.0",
"husky": "^7.0.2", "husky": "^7.0.2",
"jest": "^27.2.0", "jest": "^28.0.1",
"jsfuzz": "^1.0.15", "jsfuzz": "^1.0.15",
"mocha": "^9.1.1", "mocha": "^9.1.1",
"p-timeout": "^3.2.0", "p-timeout": "^3.2.0",
@ -86,11 +87,11 @@
"proxy": "^1.0.2", "proxy": "^1.0.2",
"proxyquire": "^2.1.3", "proxyquire": "^2.1.3",
"semver": "^7.3.5", "semver": "^7.3.5",
"sinon": "^11.1.2", "sinon": "^13.0.2",
"snazzy": "^9.0.0", "snazzy": "^9.0.0",
"standard": "^16.0.3", "standard": "^17.0.0",
"tap": "^15.0.9", "tap": "^16.1.0",
"tsd": "^0.17.0", "tsd": "^0.20.0",
"wait-on": "^6.0.0" "wait-on": "^6.0.0"
}, },
"engines": { "engines": {

View File

@ -4,6 +4,7 @@ import { EventEmitter } from 'events'
import { IncomingHttpHeaders } from 'http' import { IncomingHttpHeaders } from 'http'
import { Blob } from 'buffer' import { Blob } from 'buffer'
import BodyReadable from './readable' import BodyReadable from './readable'
import { FormData } from './formdata'
type AbortSignal = unknown; type AbortSignal = unknown;
@ -43,7 +44,7 @@ declare namespace Dispatcher {
path: string; path: string;
method: HttpMethod; method: HttpMethod;
/** Default: `null` */ /** Default: `null` */
body?: string | Buffer | Uint8Array | Readable | null; body?: string | Buffer | Uint8Array | Readable | null | FormData;
/** Default: `null` */ /** Default: `null` */
headers?: IncomingHttpHeaders | string[] | null; headers?: IncomingHttpHeaders | string[] | null;
/** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */ /** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */

View File

@ -4,6 +4,7 @@
import { Blob } from 'buffer' import { Blob } from 'buffer'
import { URL, URLSearchParams } from 'url' import { URL, URLSearchParams } from 'url'
import { ReadableStream } from 'stream/web'
import { FormData } from './formdata' import { FormData } from './formdata'
export type RequestInfo = string | URL | Request export type RequestInfo = string | URL | Request
@ -13,13 +14,6 @@ export declare function fetch (
init?: RequestInit init?: RequestInit
): Promise<Response> ): Promise<Response>
declare class ControlledAsyncIterable implements AsyncIterable<Uint8Array> {
constructor (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>)
data: AsyncIterable<Uint8Array>
disturbed: boolean
readonly [Symbol.asyncIterator]: () => AsyncIterator<Uint8Array>
}
export type BodyInit = export type BodyInit =
| ArrayBuffer | ArrayBuffer
| AsyncIterable<Uint8Array> | AsyncIterable<Uint8Array>
@ -32,7 +26,7 @@ export type BodyInit =
| string | string
export interface BodyMixin { export interface BodyMixin {
readonly body: ControlledAsyncIterable | null readonly body: ReadableStream | null
readonly bodyUsed: boolean readonly bodyUsed: boolean
readonly arrayBuffer: () => Promise<ArrayBuffer> readonly arrayBuffer: () => Promise<ArrayBuffer>
@ -139,7 +133,7 @@ export declare class Request implements BodyMixin {
readonly keepalive: boolean readonly keepalive: boolean
readonly signal: AbortSignal readonly signal: AbortSignal
readonly body: ControlledAsyncIterable | null readonly body: ReadableStream | null
readonly bodyUsed: boolean readonly bodyUsed: boolean
readonly arrayBuffer: () => Promise<ArrayBuffer> readonly arrayBuffer: () => Promise<ArrayBuffer>
@ -178,7 +172,7 @@ export declare class Response implements BodyMixin {
readonly url: string readonly url: string
readonly redirected: boolean readonly redirected: boolean
readonly body: ControlledAsyncIterable | null readonly body: ReadableStream | null
readonly bodyUsed: boolean readonly bodyUsed: boolean
readonly arrayBuffer: () => Promise<ArrayBuffer> readonly arrayBuffer: () => Promise<ArrayBuffer>

View File

@ -1,9 +1,14 @@
import Agent = require('./agent') import Agent = require('./agent')
import Dispatcher = require('./dispatcher') import Dispatcher = require('./dispatcher')
import { Interceptable } from './mock-interceptor' import { Interceptable, MockInterceptor } from './mock-interceptor'
import MockDispatch = MockInterceptor.MockDispatch;
export = MockAgent export = MockAgent
interface PendingInterceptor extends MockDispatch {
origin: string;
}
/** A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. */ /** A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. */
declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.Options> extends Dispatcher { declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.Options> extends Dispatcher {
constructor(options?: MockAgent.Options) constructor(options?: MockAgent.Options)
@ -26,6 +31,14 @@ declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.
enableNetConnect(host: ((host: string) => boolean)): void; enableNetConnect(host: ((host: string) => boolean)): void;
/** Causes all requests to throw when requests are not matched in a MockAgent intercept. */ /** Causes all requests to throw when requests are not matched in a MockAgent intercept. */
disableNetConnect(): void; disableNetConnect(): void;
pendingInterceptors(): PendingInterceptor[];
assertNoPendingInterceptors(options?: {
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
}): void;
}
interface PendingInterceptorsFormatter {
format(pendingInterceptors: readonly PendingInterceptor[]): string;
} }
declare namespace MockAgent { declare namespace MockAgent {

View File

@ -1,6 +1,6 @@
import { IncomingHttpHeaders } from 'http' import { IncomingHttpHeaders } from 'http'
import Dispatcher from './dispatcher'; import Dispatcher from './dispatcher';
import { Headers } from './fetch' import { BodyInit, Headers } from './fetch'
export { export {
Interceptable, Interceptable,
@ -71,7 +71,7 @@ declare namespace MockInterceptor {
path: string; path: string;
origin: string; origin: string;
method: string; method: string;
body?: string; body?: BodyInit | Dispatcher.DispatchOptions['body'];
headers: Headers; headers: Headers;
maxRedirections: number; maxRedirections: number;
} }

5065
deps/undici/undici.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ rm -f deps/undici/undici.js
) )
mv undici-tmp/node_modules/undici deps/undici/src mv undici-tmp/node_modules/undici deps/undici/src
mv deps/undici/src/undici.js deps/undici/undici.js mv deps/undici/src/undici-fetch.js deps/undici/undici.js
cp deps/undici/src/LICENSE deps/undici/LICENSE cp deps/undici/src/LICENSE deps/undici/LICENSE
rm -rf undici-tmp/ rm -rf undici-tmp/