Skip to content

Commit 3bd87e1

Browse files
authoredMay 2, 2022
deps: update undici to 5.1.1
PR-URL: nodejs#42939 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Mohammed Keyvanzadeh <[email protected]>
1 parent 1d8a320 commit 3bd87e1

28 files changed

+4418
-5712
lines changed
 

‎deps/undici/src/README.md

+21-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![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)
44

5-
A HTTP/1.1 client, written from scratch for Node.js.
5+
An HTTP/1.1 client, written from scratch for Node.js.
66

77
> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
88
It is also a Stranger Things reference.
@@ -65,7 +65,15 @@ for await (const data of body) {
6565
console.log('trailers', trailers)
6666
```
6767

68-
Using [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
68+
## Body Mixins
69+
70+
The `body` mixins are the most common way to format the request/response body. Mixins include:
71+
72+
- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
73+
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
74+
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
75+
76+
Example usage:
6977

7078
```js
7179
import { request } from 'undici'
@@ -83,6 +91,12 @@ console.log('data', await body.json())
8391
console.log('trailers', trailers)
8492
```
8593

94+
_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._
95+
96+
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.
97+
98+
For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
99+
86100
## Common API Methods
87101

88102
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 = {
213227

214228
#### `response.body`
215229

216-
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()`.
230+
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()`.
217231

218232
```js
219233
import {fetch} from 'undici';
@@ -228,7 +242,7 @@ Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v1
228242

229243
#### Specification Compliance
230244

231-
This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) which Undici does
245+
This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
232246
not support or does not fully implement.
233247

234248
##### Garbage Collection
@@ -239,7 +253,7 @@ The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consumi
239253
[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.
240254

241255
Garbage collection in Node is less aggressive and deterministic
242-
(due to the lack of clear idle periods that browser have through the rendering refresh rate)
256+
(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
243257
which means that leaving the release of connection resources to the garbage collector can lead
244258
to excessive connection usage, reduced performance (due to less connection re-use), and even
245259
stalls or deadlocks when running out of connections.
@@ -301,7 +315,7 @@ Returns: `Dispatcher`
301315

302316
## Specification Compliance
303317

304-
This section documents parts of the HTTP/1.1 specification which Undici does
318+
This section documents parts of the HTTP/1.1 specification that Undici does
305319
not support or does not fully implement.
306320

307321
### Expect
@@ -334,7 +348,7 @@ aborted.
334348

335349
### Manual Redirect
336350

337-
Since it is not possible to manually follow an HTTP redirect on server-side,
351+
Since it is not possible to manually follow an HTTP redirect on the server-side,
338352
Undici returns the actual response instead of an `opaqueredirect` filtered one
339353
when invoked with a `manual` redirect. This aligns `fetch()` with the other
340354
implementations in Deno and Cloudflare Workers.

‎deps/undici/src/docs/api/Dispatcher.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
193193
* **path** `string`
194194
* **method** `string`
195195
* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
196-
* **headers** `UndiciHeaders` (optional) - Default: `null`
196+
* **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
197197
* **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.
198198
* **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.
199199
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.

‎deps/undici/src/docs/api/MockAgent.md

+76
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,79 @@ mockAgent.disableNetConnect()
445445
await request('http://example.com')
446446
// Will throw
447447
```
448+
449+
### `MockAgent.pendingInterceptors()`
450+
451+
This method returns any pending interceptors registered on a mock agent. A pending interceptor meets one of the following criteria:
452+
453+
- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
454+
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
455+
- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
456+
457+
Returns: `PendingInterceptor[]` (where `PendingInterceptor` is a `MockDispatch` with an additional `origin: string`)
458+
459+
#### Example - List all pending inteceptors
460+
461+
```js
462+
const agent = new MockAgent()
463+
agent.disableNetConnect()
464+
465+
agent
466+
.get('https://example.com')
467+
.intercept({ method: 'GET', path: '/' })
468+
.reply(200, '')
469+
470+
const pendingInterceptors = agent.pendingInterceptors()
471+
// Returns [
472+
// {
473+
// timesInvoked: 0,
474+
// times: 1,
475+
// persist: false,
476+
// consumed: false,
477+
// pending: true,
478+
// path: '/',
479+
// method: 'GET',
480+
// body: undefined,
481+
// headers: undefined,
482+
// data: {
483+
// error: null,
484+
// statusCode: 200,
485+
// data: '',
486+
// headers: {},
487+
// trailers: {}
488+
// },
489+
// origin: 'https://example.com'
490+
// }
491+
// ]
492+
```
493+
494+
### `MockAgent.assertNoPendingInterceptors([options])`
495+
496+
This method throws if the mock agent has any pending interceptors. A pending interceptor meets one of the following criteria:
497+
498+
- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
499+
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
500+
- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
501+
502+
#### Example - Check that there are no pending interceptors
503+
504+
```js
505+
const agent = new MockAgent()
506+
agent.disableNetConnect()
507+
508+
agent
509+
.get('https://example.com')
510+
.intercept({ method: 'GET', path: '/' })
511+
.reply(200, '')
512+
513+
agent.assertNoPendingInterceptors()
514+
// Throws an UndiciError with the following message:
515+
//
516+
// 1 interceptor is pending:
517+
//
518+
// ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
519+
// │ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
520+
// ├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
521+
// │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │
522+
// └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
523+
```

‎deps/undici/src/docs/best-practices/mocking-request.md

+10-7
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ Undici have its own mocking [utility](../api/MockAgent.md). It allow us to inter
55
Example:
66

77
```js
8-
// index.mjs
8+
// bank.mjs
99
import { request } from 'undici'
1010

11-
export async function bankTransfer(recepient, ammount) {
12-
const { body } = await request('http://localhost:3000/bank-transfer',
11+
export async function bankTransfer(recepient, amount) {
12+
const { body } = await request('http://localhost:3000/bank-transfer',
1313
{
1414
method: 'POST',
1515
headers: {
1616
'X-TOKEN-SECRET': 'SuperSecretToken',
1717
},
18-
body: JSON.stringify({ recepient })
18+
body: JSON.stringify({
19+
recepient,
20+
amount
21+
})
1922
}
2023
)
2124
return await body.json()
@@ -28,7 +31,7 @@ And this is what the test file looks like:
2831
// index.test.mjs
2932
import { strict as assert } from 'assert'
3033
import { MockAgent, setGlobalDispatcher, } from 'undici'
31-
import { bankTransfer } from './undici.mjs'
34+
import { bankTransfer } from './bank.mjs'
3235

3336
const mockAgent = new MockAgent();
3437

@@ -46,7 +49,7 @@ mockPool.intercept({
4649
},
4750
body: JSON.stringify({
4851
recepient: '1234567890',
49-
ammount: '100'
52+
amount: '100'
5053
})
5154
}).reply(200, {
5255
message: 'transaction processed'
@@ -94,7 +97,7 @@ mockPool.intercept({
9497

9598
const badRequest = await bankTransfer('1234567890', '100')
9699
// Will throw an error
97-
// MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer':
100+
// MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer':
98101
// subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
99102
```
100103

‎deps/undici/src/index-fetch.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
const Agent = require('./lib/agent')
4+
5+
const globalDispatcher = new Agent()
6+
7+
const fetchImpl = require('./lib/fetch')
8+
module.exports.fetch = async function fetch (resource) {
9+
return fetchImpl.apply(globalDispatcher, arguments)
10+
}
11+
module.exports.FormData = require('./lib/fetch/formdata').FormData
12+
module.exports.Headers = require('./lib/fetch/headers').Headers
13+
module.exports.Response = require('./lib/fetch/response').Response
14+
module.exports.Request = require('./lib/fetch/request').Request

‎deps/undici/src/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { request, pipeline, stream, connect, upgrade } from './types/api'
1616
export * from './types/fetch'
1717
export * from './types/file'
1818
export * from './types/formdata'
19+
export { Interceptable } from './types/mock-interceptor'
1920

2021
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
2122
export default Undici

‎deps/undici/src/lib/api/api-request.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,16 @@ class RequestHandler extends AsyncResource {
8888
this.res = body
8989
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
9090

91-
this.runInAsyncScope(callback, null, null, {
92-
statusCode,
93-
headers,
94-
trailers: this.trailers,
95-
opaque,
96-
body,
97-
context
98-
})
91+
if (callback !== null) {
92+
this.runInAsyncScope(callback, null, null, {
93+
statusCode,
94+
headers,
95+
trailers: this.trailers,
96+
opaque,
97+
body,
98+
context
99+
})
100+
}
99101
}
100102

101103
onData (chunk) {

‎deps/undici/src/lib/core/request.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const kHandler = Symbol('handler')
1111

1212
const channels = {}
1313

14+
let extractBody
15+
16+
const nodeVersion = process.versions.node.split('.')
17+
const nodeMajor = Number(nodeVersion[0])
18+
const nodeMinor = Number(nodeVersion[1])
19+
1420
try {
1521
const diagnosticsChannel = require('diagnostics_channel')
1622
channels.create = diagnosticsChannel.channel('undici:request:create')
@@ -79,7 +85,7 @@ class Request {
7985
this.body = body.byteLength ? body : null
8086
} else if (typeof body === 'string') {
8187
this.body = body.length ? Buffer.from(body) : null
82-
} else if (util.isIterable(body) || util.isBlobLike(body)) {
88+
} else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
8389
this.body = body
8490
} else {
8591
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 {
126132
throw new InvalidArgumentError('headers must be an object or an array')
127133
}
128134

129-
if (util.isBlobLike(body) && this.contentType == null && body.type) {
135+
if (util.isFormDataLike(this.body)) {
136+
if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 5)) {
137+
throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.5 and newer.')
138+
}
139+
140+
if (!extractBody) {
141+
extractBody = require('../fetch/body.js').extractBody
142+
}
143+
144+
const [bodyStream, contentType] = extractBody(body)
145+
if (this.contentType == null) {
146+
this.contentType = contentType
147+
this.headers += `content-type: ${contentType}\r\n`
148+
}
149+
this.body = bodyStream.stream
150+
} else if (util.isBlobLike(body) && this.contentType == null && body.type) {
130151
this.contentType = body.type
131152
this.headers += `content-type: ${body.type}\r\n`
132153
}

‎deps/undici/src/lib/core/util.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,10 @@ function ReadableStreamFrom (iterable) {
324324
)
325325
}
326326

327+
function isFormDataLike (chunk) {
328+
return chunk && chunk.constructor && chunk.constructor.name === 'FormData'
329+
}
330+
327331
const kEnumerableProperty = Object.create(null)
328332
kEnumerableProperty.enumerable = true
329333

@@ -352,5 +356,6 @@ module.exports = {
352356
ReadableStreamFrom,
353357
isBuffer,
354358
validateHandler,
355-
getSocketInfo
359+
getSocketInfo,
360+
isFormDataLike
356361
}

‎deps/undici/src/lib/fetch/body.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function extractBody (object, keepalive = false) {
7171

7272
// Set source to a copy of the bytes held by object.
7373
source = new Uint8Array(object)
74-
} else if (object instanceof FormData) {
74+
} else if (util.isFormDataLike(object)) {
7575
const boundary = '----formdata-undici-' + Math.random()
7676
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
7777

@@ -348,7 +348,7 @@ const properties = {
348348
bodyUsed: {
349349
enumerable: true,
350350
get () {
351-
return this[kState].body && util.isDisturbed(this[kState].body.stream)
351+
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
352352
}
353353
}
354354
}

‎deps/undici/src/lib/fetch/headers.js

+75-73
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,8 @@ const {
1111
forbiddenResponseHeaderNames
1212
} = require('./constants')
1313

14-
function binarySearch (arr, val) {
15-
let low = 0
16-
let high = Math.floor(arr.length / 2)
17-
18-
while (high > low) {
19-
const mid = (high + low) >>> 1
20-
21-
if (val.localeCompare(arr[mid * 2]) > 0) {
22-
low = mid + 1
23-
} else {
24-
high = mid
25-
}
26-
}
27-
28-
return low * 2
29-
}
14+
const kHeadersMap = Symbol('headers map')
15+
const kHeadersSortedMap = Symbol('headers map sorted')
3016

3117
function normalizeAndValidateHeaderName (name) {
3218
if (name === undefined) {
@@ -91,64 +77,74 @@ function fill (headers, object) {
9177
}
9278
}
9379

94-
// TODO: Composition over inheritence? Or helper methods?
95-
class HeadersList extends Array {
80+
class HeadersList {
81+
constructor (init) {
82+
if (init instanceof HeadersList) {
83+
this[kHeadersMap] = new Map(init[kHeadersMap])
84+
this[kHeadersSortedMap] = init[kHeadersSortedMap]
85+
} else {
86+
this[kHeadersMap] = new Map(init)
87+
this[kHeadersSortedMap] = null
88+
}
89+
}
90+
9691
append (name, value) {
92+
this[kHeadersSortedMap] = null
93+
9794
const normalizedName = normalizeAndValidateHeaderName(name)
9895
const normalizedValue = normalizeAndValidateHeaderValue(name, value)
9996

100-
const index = binarySearch(this, normalizedName)
97+
const exists = this[kHeadersMap].get(normalizedName)
10198

102-
if (this[index] === normalizedName) {
103-
this[index + 1] += `, ${normalizedValue}`
99+
if (exists) {
100+
this[kHeadersMap].set(normalizedName, `${exists}, ${normalizedValue}`)
104101
} else {
105-
this.splice(index, 0, normalizedName, normalizedValue)
102+
this[kHeadersMap].set(normalizedName, `${normalizedValue}`)
106103
}
107104
}
108105

109-
delete (name) {
106+
set (name, value) {
107+
this[kHeadersSortedMap] = null
108+
110109
const normalizedName = normalizeAndValidateHeaderName(name)
110+
return this[kHeadersMap].set(normalizedName, value)
111+
}
111112

112-
const index = binarySearch(this, normalizedName)
113+
delete (name) {
114+
this[kHeadersSortedMap] = null
113115

114-
if (this[index] === normalizedName) {
115-
this.splice(index, 2)
116-
}
116+
const normalizedName = normalizeAndValidateHeaderName(name)
117+
return this[kHeadersMap].delete(normalizedName)
117118
}
118119

119120
get (name) {
120121
const normalizedName = normalizeAndValidateHeaderName(name)
121-
122-
const index = binarySearch(this, normalizedName)
123-
124-
if (this[index] === normalizedName) {
125-
return this[index + 1]
126-
}
127-
128-
return null
122+
return this[kHeadersMap].get(normalizedName) ?? null
129123
}
130124

131125
has (name) {
132126
const normalizedName = normalizeAndValidateHeaderName(name)
127+
return this[kHeadersMap].has(normalizedName)
128+
}
133129

134-
const index = binarySearch(this, normalizedName)
130+
keys () {
131+
return this[kHeadersMap].keys()
132+
}
135133

136-
return this[index] === normalizedName
134+
values () {
135+
return this[kHeadersMap].values()
137136
}
138137

139-
set (name, value) {
140-
const normalizedName = normalizeAndValidateHeaderName(name)
141-
const normalizedValue = normalizeAndValidateHeaderValue(name, value)
138+
entries () {
139+
return this[kHeadersMap].entries()
140+
}
142141

143-
const index = binarySearch(this, normalizedName)
144-
if (this[index] === normalizedName) {
145-
this[index + 1] = normalizedValue
146-
} else {
147-
this.splice(index, 0, normalizedName, normalizedValue)
148-
}
142+
[Symbol.iterator] () {
143+
return this[kHeadersMap][Symbol.iterator]()
149144
}
150145
}
151146

147+
// https://fetch.spec.whatwg.org/#headers-class
152148
class Headers {
153149
constructor (...args) {
154150
if (
@@ -161,7 +157,6 @@ class Headers {
161157
)
162158
}
163159
const init = args.length >= 1 ? args[0] ?? {} : {}
164-
165160
this[kHeadersList] = new HeadersList()
166161

167162
// The new Headers(init) constructor steps are:
@@ -287,46 +282,60 @@ class Headers {
287282
)
288283
}
289284

290-
const normalizedName = normalizeAndValidateHeaderName(String(args[0]))
291-
292285
if (this[kGuard] === 'immutable') {
293286
throw new TypeError('immutable')
294287
} else if (
295288
this[kGuard] === 'request' &&
296-
forbiddenHeaderNames.includes(normalizedName)
289+
forbiddenHeaderNames.includes(String(args[0]).toLocaleLowerCase())
297290
) {
298291
return
299292
} else if (this[kGuard] === 'request-no-cors') {
300293
// TODO
301294
} else if (
302295
this[kGuard] === 'response' &&
303-
forbiddenResponseHeaderNames.includes(normalizedName)
296+
forbiddenResponseHeaderNames.includes(String(args[0]).toLocaleLowerCase())
304297
) {
305298
return
306299
}
307300

308301
return this[kHeadersList].set(String(args[0]), String(args[1]))
309302
}
310303

311-
* keys () {
312-
const clone = this[kHeadersList].slice()
313-
for (let index = 0; index < clone.length; index += 2) {
314-
yield clone[index]
304+
get [kHeadersSortedMap] () {
305+
this[kHeadersList][kHeadersSortedMap] ??= new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1))
306+
return this[kHeadersList][kHeadersSortedMap]
307+
}
308+
309+
keys () {
310+
if (!(this instanceof Headers)) {
311+
throw new TypeError('Illegal invocation')
315312
}
313+
314+
return this[kHeadersSortedMap].keys()
316315
}
317316

318-
* values () {
319-
const clone = this[kHeadersList].slice()
320-
for (let index = 1; index < clone.length; index += 2) {
321-
yield clone[index]
317+
values () {
318+
if (!(this instanceof Headers)) {
319+
throw new TypeError('Illegal invocation')
322320
}
321+
322+
return this[kHeadersSortedMap].values()
323323
}
324324

325-
* entries () {
326-
const clone = this[kHeadersList].slice()
327-
for (let index = 0; index < clone.length; index += 2) {
328-
yield [clone[index], clone[index + 1]]
325+
entries () {
326+
if (!(this instanceof Headers)) {
327+
throw new TypeError('Illegal invocation')
329328
}
329+
330+
return this[kHeadersSortedMap].entries()
331+
}
332+
333+
[Symbol.iterator] () {
334+
if (!(this instanceof Headers)) {
335+
throw new TypeError('Illegal invocation')
336+
}
337+
338+
return this[kHeadersSortedMap]
330339
}
331340

332341
forEach (...args) {
@@ -346,15 +355,9 @@ class Headers {
346355
const callback = args[0]
347356
const thisArg = args[1]
348357

349-
const clone = this[kHeadersList].slice()
350-
for (let index = 0; index < clone.length; index += 2) {
351-
callback.call(
352-
thisArg,
353-
clone[index + 1],
354-
clone[index],
355-
this
356-
)
357-
}
358+
this[kHeadersSortedMap].forEach((value, index) => {
359+
callback.apply(thisArg, [value, index, this])
360+
})
358361
}
359362

360363
[Symbol.for('nodejs.util.inspect.custom')] () {
@@ -384,7 +387,6 @@ module.exports = {
384387
fill,
385388
Headers,
386389
HeadersList,
387-
binarySearch,
388390
normalizeAndValidateHeaderName,
389391
normalizeAndValidateHeaderValue
390392
}

‎deps/undici/src/lib/fetch/index.js

+21-17
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ async function schemeFetch (fetchParams) {
768768
const {
769769
protocol: scheme,
770770
pathname: path
771-
} = new URL(requestCurrentURL(request))
771+
} = requestCurrentURL(request)
772772

773773
// switch on request’s current URL’s scheme, and run the associated steps:
774774
switch (scheme) {
@@ -780,7 +780,7 @@ async function schemeFetch (fetchParams) {
780780
const resp = makeResponse({
781781
statusText: 'OK',
782782
headersList: [
783-
'content-type', 'text/html;charset=utf-8'
783+
['content-type', 'text/html;charset=utf-8']
784784
]
785785
})
786786

@@ -792,7 +792,7 @@ async function schemeFetch (fetchParams) {
792792
return makeNetworkError('invalid path called')
793793
}
794794
case 'blob:': {
795-
resolveObjectURL ??= require('buffer').resolveObjectURL
795+
resolveObjectURL = resolveObjectURL || require('buffer').resolveObjectURL
796796

797797
// 1. Run these steps, but abort when the ongoing fetch is terminated:
798798
// 1. Let blob be request’s current URL’s blob URL entry’s object.
@@ -871,7 +871,7 @@ async function schemeFetch (fetchParams) {
871871
return makeResponse({
872872
statusText: 'OK',
873873
headersList: [
874-
'content-type', contentType
874+
['content-type', contentType]
875875
],
876876
body: extractBody(dataURLStruct.body)[0]
877877
})
@@ -1919,8 +1919,10 @@ async function httpNetworkFetch (
19191919
origin: url.origin,
19201920
method: request.method,
19211921
body: fetchParams.controller.dispatcher[kIsMockActive] ? request.body && request.body.source : body,
1922-
headers: request.headersList,
1923-
maxRedirections: 0
1922+
headers: [...request.headersList].flat(),
1923+
maxRedirections: 0,
1924+
bodyTimeout: 300_000,
1925+
headersTimeout: 300_000
19241926
},
19251927
{
19261928
body: null,
@@ -1962,16 +1964,18 @@ async function httpNetworkFetch (
19621964
const decoders = []
19631965

19641966
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
1965-
for (const coding of codings) {
1966-
if (/(x-)?gzip/.test(coding)) {
1967-
decoders.push(zlib.createGunzip())
1968-
} else if (/(x-)?deflate/.test(coding)) {
1969-
decoders.push(zlib.createInflate())
1970-
} else if (coding === 'br') {
1971-
decoders.push(zlib.createBrotliDecompress())
1972-
} else {
1973-
decoders.length = 0
1974-
break
1967+
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) {
1968+
for (const coding of codings) {
1969+
if (/(x-)?gzip/.test(coding)) {
1970+
decoders.push(zlib.createGunzip())
1971+
} else if (/(x-)?deflate/.test(coding)) {
1972+
decoders.push(zlib.createInflate())
1973+
} else if (coding === 'br') {
1974+
decoders.push(zlib.createBrotliDecompress())
1975+
} else {
1976+
decoders.length = 0
1977+
break
1978+
}
19751979
}
19761980
}
19771981

@@ -2029,7 +2033,7 @@ async function httpNetworkFetch (
20292033

20302034
fetchParams.controller.terminate(error)
20312035

2032-
reject(makeNetworkError(error))
2036+
reject(error)
20332037
}
20342038
}
20352039
))

‎deps/undici/src/lib/fetch/request.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,15 @@ class Request {
420420
// 4. If headers is a Headers object, then for each header in its header
421421
// list, append header’s name/header’s value to this’s headers.
422422
if (headers instanceof Headers) {
423-
this[kState].headersList.push(...headers[kHeadersList])
423+
// TODO (fix): Why doesn't this work?
424+
// for (const [key, val] of headers[kHeadersList]) {
425+
// this[kHeaders].append(key, val)
426+
// }
427+
428+
this[kState].headersList = new HeadersList([
429+
...this[kState].headersList,
430+
...headers[kHeadersList]
431+
])
424432
} else {
425433
// 5. Otherwise, fill this’s headers with headers.
426434
fillHeaders(this[kState].headersList, headers)
@@ -460,6 +468,7 @@ class Request {
460468
// this’s headers.
461469
if (contentType && !this[kHeaders].has('content-type')) {
462470
this[kHeaders].append('content-type', contentType)
471+
this[kState].headersList.append('content-type', contentType)
463472
}
464473
}
465474

@@ -793,9 +802,8 @@ function makeRequest (init) {
793802
timingAllowFailed: false,
794803
...init,
795804
headersList: init.headersList
796-
? new HeadersList(...init.headersList)
797-
: new HeadersList(),
798-
urlList: init.urlList ? [...init.urlList.map((url) => new URL(url))] : []
805+
? new HeadersList(init.headersList)
806+
: new HeadersList()
799807
}
800808
request.url = request.urlList[0]
801809
return request

‎deps/undici/src/lib/fetch/response.js

+14-13
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class Response {
8181
const value = parsedURL.toString()
8282

8383
// 7. Append `Location`/value to responseObject’s response’s header list.
84-
responseObject[kState].headersList.push('location', value)
84+
responseObject[kState].headersList.append('location', value)
8585

8686
// 8. Return responseObject.
8787
return responseObject
@@ -172,7 +172,7 @@ class Response {
172172
// not contain `Content-Type`, then append `Content-Type`/Content-Type
173173
// to this’s response’s header list.
174174
if (contentType && !this.headers.has('content-type')) {
175-
this.headers.set('content-type', contentType)
175+
this.headers.append('content-type', contentType)
176176
}
177177
}
178178
}
@@ -350,7 +350,7 @@ function makeResponse (init) {
350350
statusText: '',
351351
...init,
352352
headersList: init.headersList
353-
? new HeadersList(...init.headersList)
353+
? new HeadersList(init.headersList)
354354
: new HeadersList(),
355355
urlList: init.urlList ? [...init.urlList] : []
356356
}
@@ -393,17 +393,15 @@ function makeFilteredHeadersList (headersList, filter) {
393393
get (target, prop) {
394394
// Override methods used by Headers class.
395395
if (prop === 'get' || prop === 'has') {
396-
return (name) => filter(name) ? target[prop](name) : undefined
397-
} else if (prop === 'slice') {
398-
return (...args) => {
399-
assert(args.length === 0)
400-
const arr = []
401-
for (let index = 0; index < target.length; index += 2) {
402-
if (filter(target[index])) {
403-
arr.push(target[index], target[index + 1])
396+
const defaultReturn = prop === 'has' ? false : null
397+
return (name) => filter(name) ? target[prop](name) : defaultReturn
398+
} else if (prop === Symbol.iterator) {
399+
return function * () {
400+
for (const entry of target) {
401+
if (filter(entry[0])) {
402+
yield entry
404403
}
405404
}
406-
return arr
407405
}
408406
} else {
409407
return target[prop]
@@ -423,7 +421,10 @@ function filterResponse (response, type) {
423421

424422
return makeFilteredResponse(response, {
425423
type: 'basic',
426-
headersList: makeFilteredHeadersList(response.headersList, (name) => !forbiddenResponseHeaderNames.includes(name))
424+
headersList: makeFilteredHeadersList(
425+
response.headersList,
426+
(name) => !forbiddenResponseHeaderNames.includes(name.toLowerCase())
427+
)
427428
})
428429
} else if (type === 'cors') {
429430
// A CORS filtered response is a filtered response whose type is "cors"

‎deps/undici/src/lib/fetch/util.js

+36-1
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,42 @@ function sameOrigin (A, B) {
318318

319319
// https://fetch.spec.whatwg.org/#corb-check
320320
function CORBCheck (request, response) {
321-
// TODO
321+
// 1. If request’s initiator is "download", then return allowed.
322+
if (request.initiator === 'download') {
323+
return 'allowed'
324+
}
325+
326+
// 2. If request’s current URL’s scheme is not an HTTP(S) scheme, then return allowed.
327+
if (!/^https?$/.test(request.currentURL.scheme)) {
328+
return 'allowed'
329+
}
330+
331+
// 3. Let mimeType be the result of extracting a MIME type from response’s header list.
332+
const mimeType = response.headersList.get('content-type')
333+
334+
// 4. If mimeType is failure, then return allowed.
335+
if (mimeType === '') {
336+
return 'allowed'
337+
}
338+
339+
// 5. If response’s status is 206 and mimeType is a CORB-protected MIME type, then return blocked.
340+
341+
const isCORBProtectedMIME =
342+
(/^text\/html\b/.test(mimeType) ||
343+
/^application\/javascript\b/.test(mimeType) ||
344+
/^application\/xml\b/.test(mimeType)) && !/^application\/xml\+svg\b/.test(mimeType)
345+
346+
if (response.status === 206 && isCORBProtectedMIME) {
347+
return 'blocked'
348+
}
349+
350+
// 6. If determine nosniff with response’s header list is true and mimeType is a CORB-protected MIME type or its essence is "text/plain", then return blocked.
351+
// https://fetch.spec.whatwg.org/#determinenosniff
352+
if (response.headersList.get('x-content-type-options') && isCORBProtectedMIME) {
353+
return 'blocked'
354+
}
355+
356+
// 7. Return allowed.
322357
return 'allowed'
323358
}
324359

‎deps/undici/src/lib/mock/mock-agent.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ const {
1616
const MockClient = require('./mock-client')
1717
const MockPool = require('./mock-pool')
1818
const { matchValue, buildMockOptions } = require('./mock-utils')
19-
const { InvalidArgumentError } = require('../core/errors')
19+
const { InvalidArgumentError, UndiciError } = require('../core/errors')
2020
const Dispatcher = require('../dispatcher')
21+
const Pluralizer = require('./pluralizer')
22+
const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
2123

2224
class FakeWeakRef {
2325
constructor (value) {
@@ -134,6 +136,30 @@ class MockAgent extends Dispatcher {
134136
[kGetNetConnect] () {
135137
return this[kNetConnect]
136138
}
139+
140+
pendingInterceptors () {
141+
const mockAgentClients = this[kClients]
142+
143+
return Array.from(mockAgentClients.entries())
144+
.flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin })))
145+
.filter(({ pending }) => pending)
146+
}
147+
148+
assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
149+
const pending = this.pendingInterceptors()
150+
151+
if (pending.length === 0) {
152+
return
153+
}
154+
155+
const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length)
156+
157+
throw new UndiciError(`
158+
${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending:
159+
160+
${pendingInterceptorsFormatter.format(pending)}
161+
`.trim())
162+
}
137163
}
138164

139165
module.exports = MockAgent

‎deps/undici/src/lib/mock/mock-interceptor.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const {
1212
const { InvalidArgumentError } = require('../core/errors')
1313

1414
/**
15-
* Defines the scope API for a interceptor reply
15+
* Defines the scope API for an interceptor reply
1616
*/
1717
class MockScope {
1818
constructor (mockDispatch) {
@@ -74,6 +74,9 @@ class MockInterceptor {
7474
const parsedURL = new URL(opts.path, 'data://')
7575
opts.path = parsedURL.pathname + parsedURL.search
7676
}
77+
if (typeof opts.method === 'string') {
78+
opts.method = opts.method.toUpperCase()
79+
}
7780

7881
this[kDispatchKey] = buildKey(opts)
7982
this[kDispatches] = mockDispatches

‎deps/undici/src/lib/mock/mock-utils.js

+108-17
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ function lowerCaseEntries (headers) {
3131
)
3232
}
3333

34+
/**
35+
* @param {import('../../index').Headers|string[]|Record<string, string>} headers
36+
* @param {string} key
37+
*/
38+
function getHeaderByName (headers, key) {
39+
if (Array.isArray(headers)) {
40+
for (let i = 0; i < headers.length; i += 2) {
41+
if (headers[i] === key) {
42+
return headers[i + 1]
43+
}
44+
}
45+
46+
return undefined
47+
} else if (typeof headers.get === 'function') {
48+
return headers.get(key)
49+
} else {
50+
return headers[key]
51+
}
52+
}
53+
3454
function matchHeaders (mockDispatch, headers) {
3555
if (typeof mockDispatch.headers === 'function') {
3656
if (Array.isArray(headers)) { // fetch HeadersList
@@ -51,9 +71,9 @@ function matchHeaders (mockDispatch, headers) {
5171
}
5272

5373
for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
54-
const header = typeof headers.get === 'function' ? headers.get(matchHeaderName) : headers[matchHeaderName]
74+
const headerValue = getHeaderByName(headers, matchHeaderName)
5575

56-
if (!matchValue(matchHeaderValue, header)) {
76+
if (!matchValue(matchHeaderValue, headerValue)) {
5777
return false
5878
}
5979
}
@@ -107,9 +127,9 @@ function getMockDispatch (mockDispatches, key) {
107127
}
108128

109129
function addMockDispatch (mockDispatches, key, data) {
110-
const baseData = { times: null, persist: false, consumed: false }
130+
const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false }
111131
const replyData = typeof data === 'function' ? { callback: data } : { ...data }
112-
const newMockDispatch = { ...baseData, ...key, data: { error: null, ...replyData } }
132+
const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
113133
mockDispatches.push(newMockDispatch)
114134
return newMockDispatch
115135
}
@@ -140,6 +160,80 @@ function generateKeyValues (data) {
140160
return Object.entries(data).reduce((keyValuePairs, [key, value]) => [...keyValuePairs, key, value], [])
141161
}
142162

163+
/**
164+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
165+
* @param {number} statusCode
166+
*/
167+
function getStatusText (statusCode) {
168+
switch (statusCode) {
169+
case 100: return 'Continue'
170+
case 101: return 'Switching Protocols'
171+
case 102: return 'Processing'
172+
case 103: return 'Early Hints'
173+
case 200: return 'OK'
174+
case 201: return 'Created'
175+
case 202: return 'Accepted'
176+
case 203: return 'Non-Authoritative Information'
177+
case 204: return 'No Content'
178+
case 205: return 'Reset Content'
179+
case 206: return 'Partial Content'
180+
case 207: return 'Multi-Status'
181+
case 208: return 'Already Reported'
182+
case 226: return 'IM Used'
183+
case 300: return 'Multiple Choice'
184+
case 301: return 'Moved Permanently'
185+
case 302: return 'Found'
186+
case 303: return 'See Other'
187+
case 304: return 'Not Modified'
188+
case 305: return 'Use Proxy'
189+
case 306: return 'unused'
190+
case 307: return 'Temporary Redirect'
191+
case 308: return 'Permanent Redirect'
192+
case 400: return 'Bad Request'
193+
case 401: return 'Unauthorized'
194+
case 402: return 'Payment Required'
195+
case 403: return 'Forbidden'
196+
case 404: return 'Not Found'
197+
case 405: return 'Method Not Allowed'
198+
case 406: return 'Not Acceptable'
199+
case 407: return 'Proxy Authentication Required'
200+
case 408: return 'Request Timeout'
201+
case 409: return 'Conflict'
202+
case 410: return 'Gone'
203+
case 411: return 'Length Required'
204+
case 412: return 'Precondition Failed'
205+
case 413: return 'Payload Too Large'
206+
case 414: return 'URI Too Large'
207+
case 415: return 'Unsupported Media Type'
208+
case 416: return 'Range Not Satisfiable'
209+
case 417: return 'Expectation Failed'
210+
case 418: return 'I\'m a teapot'
211+
case 421: return 'Misdirected Request'
212+
case 422: return 'Unprocessable Entity'
213+
case 423: return 'Locked'
214+
case 424: return 'Failed Dependency'
215+
case 425: return 'Too Early'
216+
case 426: return 'Upgrade Required'
217+
case 428: return 'Precondition Required'
218+
case 429: return 'Too Many Requests'
219+
case 431: return 'Request Header Fields Too Large'
220+
case 451: return 'Unavailable For Legal Reasons'
221+
case 500: return 'Internal Server Error'
222+
case 501: return 'Not Implemented'
223+
case 502: return 'Bad Gateway'
224+
case 503: return 'Service Unavailable'
225+
case 504: return 'Gateway Timeout'
226+
case 505: return 'HTTP Version Not Supported'
227+
case 506: return 'Variant Also Negotiates'
228+
case 507: return 'Insufficient Storage'
229+
case 508: return 'Loop Detected'
230+
case 510: return 'Not Extended'
231+
case 511: return 'Network Authentication Required'
232+
default:
233+
throw new ReferenceError(`Unknown status code "${statusCode}"!`)
234+
}
235+
}
236+
143237
async function getResponse (body) {
144238
const buffers = []
145239
for await (const data of body) {
@@ -156,25 +250,20 @@ function mockDispatch (opts, handler) {
156250
const key = buildKey(opts)
157251
const mockDispatch = getMockDispatch(this[kDispatches], key)
158252

253+
mockDispatch.timesInvoked++
254+
159255
// Here's where we resolve a callback if a callback is present for the dispatch data.
160256
if (mockDispatch.data.callback) {
161257
mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) }
162258
}
163259

164260
// Parse mockDispatch data
165261
const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch
166-
let { times } = mockDispatch
167-
if (typeof times === 'number' && times > 0) {
168-
times = --mockDispatch.times
169-
}
262+
const { timesInvoked, times } = mockDispatch
170263

171-
// If persist is true, skip
172-
// Or if times is a number and > 0, skip
173-
// Otherwise, mark as consumed
174-
175-
if (!(persist === true || (typeof times === 'number' && times > 0))) {
176-
mockDispatch.consumed = true
177-
}
264+
// If it's used up and not persistent, mark as consumed
265+
mockDispatch.consumed = !persist && timesInvoked >= times
266+
mockDispatch.pending = timesInvoked < times
178267

179268
// If specified, trigger dispatch error
180269
if (error !== null) {
@@ -197,7 +286,7 @@ function mockDispatch (opts, handler) {
197286
const responseHeaders = generateKeyValues(headers)
198287
const responseTrailers = generateKeyValues(trailers)
199288

200-
handler.onHeaders(statusCode, responseHeaders, resume)
289+
handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
201290
handler.onData(Buffer.from(responseData))
202291
handler.onComplete(responseTrailers)
203292
deleteMockDispatch(mockDispatches, key)
@@ -264,8 +353,10 @@ module.exports = {
264353
generateKeyValues,
265354
matchValue,
266355
getResponse,
356+
getStatusText,
267357
mockDispatch,
268358
buildMockDispatch,
269359
checkNetConnect,
270-
buildMockOptions
360+
buildMockOptions,
361+
getHeaderByName
271362
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict'
2+
3+
const { Transform } = require('stream')
4+
const { Console } = require('console')
5+
6+
/**
7+
* Gets the output of `console.table(…)` as a string.
8+
*/
9+
module.exports = class PendingInterceptorsFormatter {
10+
constructor ({ disableColors } = {}) {
11+
this.transform = new Transform({
12+
transform (chunk, _enc, cb) {
13+
cb(null, chunk)
14+
}
15+
})
16+
17+
this.logger = new Console({
18+
stdout: this.transform,
19+
inspectOptions: {
20+
colors: !disableColors && !process.env.CI
21+
}
22+
})
23+
}
24+
25+
format (pendingInterceptors) {
26+
const withPrettyHeaders = pendingInterceptors.map(
27+
({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
28+
Method: method,
29+
Origin: origin,
30+
Path: path,
31+
'Status code': statusCode,
32+
Persistent: persist ? '✅' : '❌',
33+
Invocations: timesInvoked,
34+
Remaining: persist ? Infinity : times - timesInvoked
35+
}))
36+
37+
this.logger.table(withPrettyHeaders)
38+
return this.transform.read().toString()
39+
}
40+
}
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict'
2+
3+
const singulars = {
4+
pronoun: 'it',
5+
is: 'is',
6+
was: 'was',
7+
this: 'this'
8+
}
9+
10+
const plurals = {
11+
pronoun: 'they',
12+
is: 'are',
13+
was: 'were',
14+
this: 'these'
15+
}
16+
17+
module.exports = class Pluralizer {
18+
constructor (singular, plural) {
19+
this.singular = singular
20+
this.plural = plural
21+
}
22+
23+
pluralize (count) {
24+
const one = count === 1
25+
const keys = one ? singulars : plurals
26+
const noun = one ? this.singular : this.plural
27+
return { ...keys, count, noun }
28+
}
29+
}

‎deps/undici/src/lib/proxy-agent.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class ProxyAgent extends DispatcherBase {
2323
origin: this[kProxy].uri,
2424
path: opts.origin + opts.path,
2525
headers: {
26-
...opts.headers,
26+
...buildHeaders(opts.headers),
2727
host
2828
}
2929
},
@@ -55,4 +55,25 @@ function buildProxyOptions (opts) {
5555
}
5656
}
5757

58+
/**
59+
* @param {string[] | Record<string, string>} headers
60+
* @returns {Record<string, string>}
61+
*/
62+
function buildHeaders (headers) {
63+
// When using undici.fetch, the headers list is stored
64+
// as an array.
65+
if (Array.isArray(headers)) {
66+
/** @type {Record<string, string>} */
67+
const headersPair = {}
68+
69+
for (let i = 0; i < headers.length; i += 2) {
70+
headersPair[headers[i]] = headers[i + 1]
71+
}
72+
73+
return headersPair
74+
}
75+
76+
return headers
77+
}
78+
5879
module.exports = ProxyAgent

‎deps/undici/src/package.json

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "undici",
3-
"version": "5.0.0",
3+
"version": "5.1.1",
44
"description": "An HTTP/1.1 client, written from scratch for Node.js",
55
"homepage": "https://undici.nodejs.org",
66
"bugs": {
@@ -35,12 +35,13 @@
3535
"files": [
3636
"*.d.ts",
3737
"index.js",
38+
"index-fetch.js",
3839
"lib",
3940
"types",
4041
"docs"
4142
],
4243
"scripts": {
43-
"build:node": "npx esbuild@0.14.25 index.js --bundle --platform=node --outfile=undici.js",
44+
"build:node": "npx esbuild@0.14.38 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js",
4445
"prebuild:wasm": "docker build -t llhttp_wasm_builder -f build/Dockerfile .",
4546
"build:wasm": "node build/wasm.js --docker",
4647
"lint": "standard | snazzy",
@@ -63,34 +64,34 @@
6364
"fuzz": "jsfuzz test/fuzzing/fuzz.js corpus"
6465
},
6566
"devDependencies": {
66-
"@sinonjs/fake-timers": "^7.0.5",
67-
"@types/node": "^16.9.1",
67+
"@sinonjs/fake-timers": "^9.1.2",
68+
"@types/node": "^17.0.29",
6869
"abort-controller": "^3.0.0",
6970
"busboy": "^0.3.1",
7071
"chai": "^4.3.4",
7172
"chai-as-promised": "^7.1.1",
7273
"chai-iterator": "^3.0.2",
7374
"chai-string": "^1.5.0",
74-
"concurrently": "^6.2.1",
75+
"concurrently": "^7.1.0",
7576
"cronometro": "^0.8.0",
7677
"delay": "^5.0.0",
7778
"docsify-cli": "^4.4.3",
7879
"formdata-node": "^4.3.1",
7980
"https-pem": "^2.0.0",
8081
"husky": "^7.0.2",
81-
"jest": "^27.2.0",
82+
"jest": "^28.0.1",
8283
"jsfuzz": "^1.0.15",
8384
"mocha": "^9.1.1",
8485
"p-timeout": "^3.2.0",
8586
"pre-commit": "^1.2.2",
8687
"proxy": "^1.0.2",
8788
"proxyquire": "^2.1.3",
8889
"semver": "^7.3.5",
89-
"sinon": "^11.1.2",
90+
"sinon": "^13.0.2",
9091
"snazzy": "^9.0.0",
91-
"standard": "^16.0.3",
92-
"tap": "^15.0.9",
93-
"tsd": "^0.17.0",
92+
"standard": "^17.0.0",
93+
"tap": "^16.1.0",
94+
"tsd": "^0.20.0",
9495
"wait-on": "^6.0.0"
9596
},
9697
"engines": {

‎deps/undici/src/types/dispatcher.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EventEmitter } from 'events'
44
import { IncomingHttpHeaders } from 'http'
55
import { Blob } from 'buffer'
66
import BodyReadable from './readable'
7+
import { FormData } from './formdata'
78

89
type AbortSignal = unknown;
910

@@ -43,7 +44,7 @@ declare namespace Dispatcher {
4344
path: string;
4445
method: HttpMethod;
4546
/** Default: `null` */
46-
body?: string | Buffer | Uint8Array | Readable | null;
47+
body?: string | Buffer | Uint8Array | Readable | null | FormData;
4748
/** Default: `null` */
4849
headers?: IncomingHttpHeaders | string[] | null;
4950
/** 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`. */

‎deps/undici/src/types/fetch.d.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { Blob } from 'buffer'
66
import { URL, URLSearchParams } from 'url'
7+
import { ReadableStream } from 'stream/web'
78
import { FormData } from './formdata'
89

910
export type RequestInfo = string | URL | Request
@@ -13,13 +14,6 @@ export declare function fetch (
1314
init?: RequestInit
1415
): Promise<Response>
1516

16-
declare class ControlledAsyncIterable implements AsyncIterable<Uint8Array> {
17-
constructor (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>)
18-
data: AsyncIterable<Uint8Array>
19-
disturbed: boolean
20-
readonly [Symbol.asyncIterator]: () => AsyncIterator<Uint8Array>
21-
}
22-
2317
export type BodyInit =
2418
| ArrayBuffer
2519
| AsyncIterable<Uint8Array>
@@ -32,7 +26,7 @@ export type BodyInit =
3226
| string
3327

3428
export interface BodyMixin {
35-
readonly body: ControlledAsyncIterable | null
29+
readonly body: ReadableStream | null
3630
readonly bodyUsed: boolean
3731

3832
readonly arrayBuffer: () => Promise<ArrayBuffer>
@@ -139,7 +133,7 @@ export declare class Request implements BodyMixin {
139133
readonly keepalive: boolean
140134
readonly signal: AbortSignal
141135

142-
readonly body: ControlledAsyncIterable | null
136+
readonly body: ReadableStream | null
143137
readonly bodyUsed: boolean
144138

145139
readonly arrayBuffer: () => Promise<ArrayBuffer>
@@ -178,7 +172,7 @@ export declare class Response implements BodyMixin {
178172
readonly url: string
179173
readonly redirected: boolean
180174

181-
readonly body: ControlledAsyncIterable | null
175+
readonly body: ReadableStream | null
182176
readonly bodyUsed: boolean
183177

184178
readonly arrayBuffer: () => Promise<ArrayBuffer>

‎deps/undici/src/types/mock-agent.d.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import Agent = require('./agent')
22
import Dispatcher = require('./dispatcher')
3-
import { Interceptable } from './mock-interceptor'
3+
import { Interceptable, MockInterceptor } from './mock-interceptor'
4+
import MockDispatch = MockInterceptor.MockDispatch;
45

56
export = MockAgent
67

8+
interface PendingInterceptor extends MockDispatch {
9+
origin: string;
10+
}
11+
712
/** A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. */
813
declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.Options> extends Dispatcher {
914
constructor(options?: MockAgent.Options)
@@ -26,6 +31,14 @@ declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.
2631
enableNetConnect(host: ((host: string) => boolean)): void;
2732
/** Causes all requests to throw when requests are not matched in a MockAgent intercept. */
2833
disableNetConnect(): void;
34+
pendingInterceptors(): PendingInterceptor[];
35+
assertNoPendingInterceptors(options?: {
36+
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
37+
}): void;
38+
}
39+
40+
interface PendingInterceptorsFormatter {
41+
format(pendingInterceptors: readonly PendingInterceptor[]): string;
2942
}
3043

3144
declare namespace MockAgent {

‎deps/undici/src/types/mock-interceptor.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IncomingHttpHeaders } from 'http'
22
import Dispatcher from './dispatcher';
3-
import { Headers } from './fetch'
3+
import { BodyInit, Headers } from './fetch'
44

55
export {
66
Interceptable,
@@ -71,7 +71,7 @@ declare namespace MockInterceptor {
7171
path: string;
7272
origin: string;
7373
method: string;
74-
body?: string;
74+
body?: BodyInit | Dispatcher.DispatchOptions['body'];
7575
headers: Headers;
7676
maxRedirections: number;
7777
}

‎deps/undici/undici.js

+3,832-5,531
Large diffs are not rendered by default.

‎tools/update-undici.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ rm -f deps/undici/undici.js
2929
)
3030

3131
mv undici-tmp/node_modules/undici deps/undici/src
32-
mv deps/undici/src/undici.js deps/undici/undici.js
32+
mv deps/undici/src/undici-fetch.js deps/undici/undici.js
3333
cp deps/undici/src/LICENSE deps/undici/LICENSE
3434

3535
rm -rf undici-tmp/

0 commit comments

Comments
 (0)
Please sign in to comment.