undici 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/docs/api/Dispatcher.md +3 -1
- package/docs/best-practices/mocking-request.md +32 -0
- package/index.d.ts +1 -0
- package/lib/api/api-request.js +12 -3
- package/lib/core/errors.js +14 -0
- package/lib/core/request.js +7 -3
- package/lib/core/util.js +47 -1
- package/lib/fetch/file.js +0 -8
- package/lib/fetch/formdata.js +3 -5
- package/lib/fetch/index.js +3 -15
- package/lib/fetch/request.js +0 -4
- package/lib/fetch/response.js +106 -63
- package/lib/fetch/util.js +20 -43
- package/lib/mock/mock-agent.js +6 -0
- package/lib/mock/mock-utils.js +1 -2
- package/lib/proxy-agent.js +0 -1
- package/package.json +3 -2
- package/types/diagnostics-channel.d.ts +66 -0
- package/types/dispatcher.d.ts +4 -0
- package/types/fetch.d.ts +14 -13
package/README.md
CHANGED
|
@@ -198,7 +198,7 @@ You can pass an optional dispatcher to `fetch` as:
|
|
|
198
198
|
|
|
199
199
|
```js
|
|
200
200
|
import { fetch, Agent } from 'undici'
|
|
201
|
-
|
|
201
|
+
|
|
202
202
|
const res = await fetch('https://example.com', {
|
|
203
203
|
// Mocks are also supported
|
|
204
204
|
dispatcher: new Agent({
|
|
@@ -375,6 +375,7 @@ Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
|
|
|
375
375
|
* [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
|
|
376
376
|
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
|
|
377
377
|
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
|
|
378
|
+
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
|
|
378
379
|
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
|
|
379
380
|
* [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
|
|
380
381
|
* [__Tomas Della Vedova__](https://github.com/delvedor), <https://www.npmjs.com/~delvedor>
|
package/docs/api/Dispatcher.md
CHANGED
|
@@ -194,18 +194,20 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
|
|
|
194
194
|
* **method** `string`
|
|
195
195
|
* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
|
|
196
196
|
* **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
|
|
197
|
+
* **query** `Record<string, any> | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead.
|
|
197
198
|
* **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.
|
|
198
199
|
* **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.
|
|
199
200
|
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
|
|
200
201
|
* **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
|
|
201
202
|
* **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds.
|
|
203
|
+
* **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
|
|
202
204
|
|
|
203
205
|
#### Parameter: `DispatchHandler`
|
|
204
206
|
|
|
205
207
|
* **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
|
|
206
208
|
* **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
|
|
207
209
|
* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
|
|
208
|
-
* **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
|
|
210
|
+
* **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
|
|
209
211
|
* **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests.
|
|
210
212
|
* **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
|
|
211
213
|
* **onBodySent** `(chunk: string | Buffer | Uint8Array) => void` - Invoked when a body chunk is sent to the server. Not required. For a stream or iterable body this will be invoked for every chunk. For other body types, it will be invoked once after the body is sent.
|
|
@@ -101,4 +101,36 @@ const badRequest = await bankTransfer('1234567890', '100')
|
|
|
101
101
|
// subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
## Reply with data based on request
|
|
104
105
|
|
|
106
|
+
If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
mockPool.intercept({
|
|
110
|
+
path: '/bank-transfer',
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
'X-TOKEN-SECRET': 'SuperSecretToken',
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
recepient: '1234567890',
|
|
117
|
+
amount: '100'
|
|
118
|
+
})
|
|
119
|
+
}).reply(200, (opts) => {
|
|
120
|
+
// do something with opts
|
|
121
|
+
|
|
122
|
+
return { message: 'transaction processed' }
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
in this case opts will be
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
{
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
|
|
132
|
+
body: '{"recepient":"1234567890","amount":"100"}',
|
|
133
|
+
origin: 'http://localhost:3000',
|
|
134
|
+
path: '/bank-transfer'
|
|
135
|
+
}
|
|
136
|
+
```
|
package/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { request, pipeline, stream, connect, upgrade } from './types/api'
|
|
|
16
16
|
export * from './types/fetch'
|
|
17
17
|
export * from './types/file'
|
|
18
18
|
export * from './types/formdata'
|
|
19
|
+
export * from './types/diagnostics-channel'
|
|
19
20
|
export { Interceptable } from './types/mock-interceptor'
|
|
20
21
|
|
|
21
22
|
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
|
package/lib/api/api-request.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const Readable = require('./readable')
|
|
4
4
|
const {
|
|
5
5
|
InvalidArgumentError,
|
|
6
|
-
RequestAbortedError
|
|
6
|
+
RequestAbortedError,
|
|
7
|
+
ResponseStatusCodeError
|
|
7
8
|
} = require('../core/errors')
|
|
8
9
|
const util = require('../core/util')
|
|
9
10
|
const { AsyncResource } = require('async_hooks')
|
|
@@ -15,7 +16,7 @@ class RequestHandler extends AsyncResource {
|
|
|
15
16
|
throw new InvalidArgumentError('invalid opts')
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const { signal, method, opaque, body, onInfo, responseHeaders } = opts
|
|
19
|
+
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts
|
|
19
20
|
|
|
20
21
|
try {
|
|
21
22
|
if (typeof callback !== 'function') {
|
|
@@ -51,6 +52,7 @@ class RequestHandler extends AsyncResource {
|
|
|
51
52
|
this.trailers = {}
|
|
52
53
|
this.context = null
|
|
53
54
|
this.onInfo = onInfo || null
|
|
55
|
+
this.throwOnError = throwOnError
|
|
54
56
|
|
|
55
57
|
if (util.isStream(body)) {
|
|
56
58
|
body.on('error', (err) => {
|
|
@@ -70,7 +72,7 @@ class RequestHandler extends AsyncResource {
|
|
|
70
72
|
this.context = context
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
onHeaders (statusCode, rawHeaders, resume) {
|
|
75
|
+
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
|
74
76
|
const { callback, opaque, abort, context } = this
|
|
75
77
|
|
|
76
78
|
if (statusCode < 200) {
|
|
@@ -89,6 +91,13 @@ class RequestHandler extends AsyncResource {
|
|
|
89
91
|
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
|
90
92
|
|
|
91
93
|
if (callback !== null) {
|
|
94
|
+
if (this.throwOnError && statusCode >= 400) {
|
|
95
|
+
this.runInAsyncScope(callback, null,
|
|
96
|
+
new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)
|
|
97
|
+
)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
this.runInAsyncScope(callback, null, null, {
|
|
93
102
|
statusCode,
|
|
94
103
|
headers,
|
package/lib/core/errors.js
CHANGED
|
@@ -56,6 +56,19 @@ class BodyTimeoutError extends UndiciError {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
class ResponseStatusCodeError extends UndiciError {
|
|
60
|
+
constructor (message, statusCode, headers) {
|
|
61
|
+
super(message)
|
|
62
|
+
Error.captureStackTrace(this, ResponseStatusCodeError)
|
|
63
|
+
this.name = 'ResponseStatusCodeError'
|
|
64
|
+
this.message = message || 'Response Status Code Error'
|
|
65
|
+
this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
|
|
66
|
+
this.status = statusCode
|
|
67
|
+
this.statusCode = statusCode
|
|
68
|
+
this.headers = headers
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
class InvalidArgumentError extends UndiciError {
|
|
60
73
|
constructor (message) {
|
|
61
74
|
super(message)
|
|
@@ -186,6 +199,7 @@ module.exports = {
|
|
|
186
199
|
BodyTimeoutError,
|
|
187
200
|
RequestContentLengthMismatchError,
|
|
188
201
|
ConnectTimeoutError,
|
|
202
|
+
ResponseStatusCodeError,
|
|
189
203
|
InvalidArgumentError,
|
|
190
204
|
InvalidReturnValueError,
|
|
191
205
|
RequestAbortedError,
|
package/lib/core/request.js
CHANGED
|
@@ -4,8 +4,8 @@ const {
|
|
|
4
4
|
InvalidArgumentError,
|
|
5
5
|
NotSupportedError
|
|
6
6
|
} = require('./errors')
|
|
7
|
-
const util = require('./util')
|
|
8
7
|
const assert = require('assert')
|
|
8
|
+
const util = require('./util')
|
|
9
9
|
|
|
10
10
|
const kHandler = Symbol('handler')
|
|
11
11
|
|
|
@@ -38,11 +38,13 @@ class Request {
|
|
|
38
38
|
method,
|
|
39
39
|
body,
|
|
40
40
|
headers,
|
|
41
|
+
query,
|
|
41
42
|
idempotent,
|
|
42
43
|
blocking,
|
|
43
44
|
upgrade,
|
|
44
45
|
headersTimeout,
|
|
45
|
-
bodyTimeout
|
|
46
|
+
bodyTimeout,
|
|
47
|
+
throwOnError
|
|
46
48
|
}, handler) {
|
|
47
49
|
if (typeof path !== 'string') {
|
|
48
50
|
throw new InvalidArgumentError('path must be a string')
|
|
@@ -70,6 +72,8 @@ class Request {
|
|
|
70
72
|
|
|
71
73
|
this.bodyTimeout = bodyTimeout
|
|
72
74
|
|
|
75
|
+
this.throwOnError = throwOnError === true
|
|
76
|
+
|
|
73
77
|
this.method = method
|
|
74
78
|
|
|
75
79
|
if (body == null) {
|
|
@@ -97,7 +101,7 @@ class Request {
|
|
|
97
101
|
|
|
98
102
|
this.upgrade = upgrade || null
|
|
99
103
|
|
|
100
|
-
this.path = path
|
|
104
|
+
this.path = query ? util.buildURL(path, query) : path
|
|
101
105
|
|
|
102
106
|
this.origin = origin
|
|
103
107
|
|
package/lib/core/util.js
CHANGED
|
@@ -26,6 +26,51 @@ function isBlobLike (object) {
|
|
|
26
26
|
)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function isObject (val) {
|
|
30
|
+
return val !== null && typeof val === 'object'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// this escapes all non-uri friendly characters
|
|
34
|
+
function encode (val) {
|
|
35
|
+
return encodeURIComponent(val)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license)
|
|
39
|
+
function buildURL (url, queryParams) {
|
|
40
|
+
if (url.includes('?') || url.includes('#')) {
|
|
41
|
+
throw new Error('Query params cannot be passed when url already contains "?" or "#".')
|
|
42
|
+
}
|
|
43
|
+
if (!isObject(queryParams)) {
|
|
44
|
+
throw new Error('Query params must be an object')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parts = []
|
|
48
|
+
for (let [key, val] of Object.entries(queryParams)) {
|
|
49
|
+
if (val === null || typeof val === 'undefined') {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!Array.isArray(val)) {
|
|
54
|
+
val = [val]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const v of val) {
|
|
58
|
+
if (isObject(v)) {
|
|
59
|
+
throw new Error('Passing object as a query param is not supported, please serialize to string up-front')
|
|
60
|
+
}
|
|
61
|
+
parts.push(encode(key) + '=' + encode(v))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const serializedParams = parts.join('&')
|
|
66
|
+
|
|
67
|
+
if (serializedParams) {
|
|
68
|
+
url += '?' + serializedParams
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return url
|
|
72
|
+
}
|
|
73
|
+
|
|
29
74
|
function parseURL (url) {
|
|
30
75
|
if (typeof url === 'string') {
|
|
31
76
|
url = new URL(url)
|
|
@@ -357,5 +402,6 @@ module.exports = {
|
|
|
357
402
|
isBuffer,
|
|
358
403
|
validateHandler,
|
|
359
404
|
getSocketInfo,
|
|
360
|
-
isFormDataLike
|
|
405
|
+
isFormDataLike,
|
|
406
|
+
buildURL
|
|
361
407
|
}
|
package/lib/fetch/file.js
CHANGED
|
@@ -69,10 +69,6 @@ class File extends Blob {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
get [Symbol.toStringTag] () {
|
|
72
|
-
if (!(this instanceof File)) {
|
|
73
|
-
throw new TypeError('Illegal invocation')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
72
|
return this.constructor.name
|
|
77
73
|
}
|
|
78
74
|
}
|
|
@@ -190,10 +186,6 @@ class FileLike {
|
|
|
190
186
|
}
|
|
191
187
|
|
|
192
188
|
get [Symbol.toStringTag] () {
|
|
193
|
-
if (!(this instanceof FileLike)) {
|
|
194
|
-
throw new TypeError('Illegal invocation')
|
|
195
|
-
}
|
|
196
|
-
|
|
197
189
|
return 'File'
|
|
198
190
|
}
|
|
199
191
|
}
|
package/lib/fetch/formdata.js
CHANGED
|
@@ -6,6 +6,8 @@ const { File, FileLike } = require('./file')
|
|
|
6
6
|
const { Blob } = require('buffer')
|
|
7
7
|
|
|
8
8
|
class FormData {
|
|
9
|
+
static name = 'FormData'
|
|
10
|
+
|
|
9
11
|
constructor (...args) {
|
|
10
12
|
if (args.length > 0 && !(args[0]?.constructor?.name === 'HTMLFormElement')) {
|
|
11
13
|
throw new TypeError(
|
|
@@ -182,10 +184,6 @@ class FormData {
|
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
get [Symbol.toStringTag] () {
|
|
185
|
-
if (!(this instanceof FormData)) {
|
|
186
|
-
throw new TypeError('Illegal invocation')
|
|
187
|
-
}
|
|
188
|
-
|
|
189
187
|
return this.constructor.name
|
|
190
188
|
}
|
|
191
189
|
|
|
@@ -269,4 +267,4 @@ function makeEntry (name, value, filename) {
|
|
|
269
267
|
return entry
|
|
270
268
|
}
|
|
271
269
|
|
|
272
|
-
module.exports = { FormData
|
|
270
|
+
module.exports = { FormData }
|
package/lib/fetch/index.js
CHANGED
|
@@ -31,7 +31,6 @@ const {
|
|
|
31
31
|
coarsenedSharedCurrentTime,
|
|
32
32
|
createDeferredPromise,
|
|
33
33
|
isBlobLike,
|
|
34
|
-
CORBCheck,
|
|
35
34
|
sameOrigin,
|
|
36
35
|
isCancelled,
|
|
37
36
|
isAborted
|
|
@@ -52,7 +51,6 @@ const EE = require('events')
|
|
|
52
51
|
const { Readable, pipeline } = require('stream')
|
|
53
52
|
const { isErrored, isReadable } = require('../core/util')
|
|
54
53
|
const { dataURLProcessor } = require('./dataURL')
|
|
55
|
-
const { kIsMockActive } = require('../mock/mock-symbols')
|
|
56
54
|
const { TransformStream } = require('stream/web')
|
|
57
55
|
|
|
58
56
|
/** @type {import('buffer').resolveObjectURL} */
|
|
@@ -588,18 +586,8 @@ async function mainFetch (fetchParams, recursive = false) {
|
|
|
588
586
|
// 2. Set request’s response tainting to "opaque".
|
|
589
587
|
request.responseTainting = 'opaque'
|
|
590
588
|
|
|
591
|
-
// 3.
|
|
592
|
-
|
|
593
|
-
const noCorsResponse = await schemeFetch(fetchParams)
|
|
594
|
-
|
|
595
|
-
// 4. If noCorsResponse is a filtered response or the CORB check with
|
|
596
|
-
// request and noCorsResponse returns allowed, then return noCorsResponse.
|
|
597
|
-
if (noCorsResponse.status === 0 || CORBCheck(request, noCorsResponse) === 'allowed') {
|
|
598
|
-
return noCorsResponse
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// 5. Return a new response whose status is noCorsResponse’s status.
|
|
602
|
-
return makeResponse({ status: noCorsResponse.status })
|
|
589
|
+
// 3. Return the result of running scheme fetch given fetchParams.
|
|
590
|
+
return await schemeFetch(fetchParams)
|
|
603
591
|
}
|
|
604
592
|
|
|
605
593
|
// request’s current URL’s scheme is not an HTTP(S) scheme
|
|
@@ -1923,7 +1911,7 @@ async function httpNetworkFetch (
|
|
|
1923
1911
|
path: url.pathname + url.search,
|
|
1924
1912
|
origin: url.origin,
|
|
1925
1913
|
method: request.method,
|
|
1926
|
-
body: fetchParams.controller.dispatcher
|
|
1914
|
+
body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body,
|
|
1927
1915
|
headers: [...request.headersList].flat(),
|
|
1928
1916
|
maxRedirections: 0,
|
|
1929
1917
|
bodyTimeout: 300_000,
|
package/lib/fetch/request.js
CHANGED
package/lib/fetch/response.js
CHANGED
|
@@ -5,7 +5,7 @@ const { AbortError } = require('../core/errors')
|
|
|
5
5
|
const { extractBody, cloneBody, mixinBody } = require('./body')
|
|
6
6
|
const util = require('../core/util')
|
|
7
7
|
const { kEnumerableProperty } = util
|
|
8
|
-
const { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted } = require('./util')
|
|
8
|
+
const { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted, serializeJavascriptValueToJSONString } = require('./util')
|
|
9
9
|
const {
|
|
10
10
|
redirectStatus,
|
|
11
11
|
nullBodyStatus,
|
|
@@ -35,6 +35,50 @@ class Response {
|
|
|
35
35
|
return responseObject
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// https://fetch.spec.whatwg.org/#dom-response-json
|
|
39
|
+
static json (data, init = {}) {
|
|
40
|
+
if (arguments.length === 0) {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
'Failed to execute \'json\' on \'Response\': 1 argument required, but 0 present.'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (init === null || typeof init !== 'object') {
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
`Failed to execute 'json' on 'Response': init must be a RequestInit, found ${typeof init}.`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
init = {
|
|
53
|
+
status: 200,
|
|
54
|
+
statusText: '',
|
|
55
|
+
headers: new HeadersList(),
|
|
56
|
+
...init
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
|
|
60
|
+
const bytes = new TextEncoder('utf-8').encode(
|
|
61
|
+
serializeJavascriptValueToJSONString(data)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// 2. Let body be the result of extracting bytes.
|
|
65
|
+
const body = extractBody(bytes)
|
|
66
|
+
|
|
67
|
+
// 3. Let responseObject be the result of creating a Response object, given a new response,
|
|
68
|
+
// "response", and this’s relevant Realm.
|
|
69
|
+
const relevantRealm = { settingsObject: {} }
|
|
70
|
+
const responseObject = new Response()
|
|
71
|
+
responseObject[kRealm] = relevantRealm
|
|
72
|
+
responseObject[kHeaders][kGuard] = 'response'
|
|
73
|
+
responseObject[kHeaders][kRealm] = relevantRealm
|
|
74
|
+
|
|
75
|
+
// 4. Perform initialize a response given responseObject, init, and (body, "application/json").
|
|
76
|
+
initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
|
|
77
|
+
|
|
78
|
+
// 5. Return responseObject.
|
|
79
|
+
return responseObject
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
// Creates a redirect Response that redirects to url with status status.
|
|
39
83
|
static redirect (...args) {
|
|
40
84
|
const relevantRealm = { settingsObject: {} }
|
|
@@ -105,34 +149,10 @@ class Response {
|
|
|
105
149
|
// TODO
|
|
106
150
|
this[kRealm] = { settingsObject: {} }
|
|
107
151
|
|
|
108
|
-
// 1.
|
|
109
|
-
// throw a RangeError.
|
|
110
|
-
if ('status' in init && init.status !== undefined) {
|
|
111
|
-
if (!Number.isFinite(init.status)) {
|
|
112
|
-
throw new TypeError()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (init.status < 200 || init.status > 599) {
|
|
116
|
-
throw new RangeError(
|
|
117
|
-
`Failed to construct 'Response': The status provided (${init.status}) is outside the range [200, 599].`
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if ('statusText' in init && init.statusText !== undefined) {
|
|
123
|
-
// 2. If init["statusText"] does not match the reason-phrase token
|
|
124
|
-
// production, then throw a TypeError.
|
|
125
|
-
// See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
|
|
126
|
-
// reason-phrase = *( HTAB / SP / VCHAR / obs-text )
|
|
127
|
-
if (!isValidReasonPhrase(String(init.statusText))) {
|
|
128
|
-
throw new TypeError('Invalid statusText')
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// 3. Set this’s response to a new response.
|
|
152
|
+
// 1. Set this’s response to a new response.
|
|
133
153
|
this[kState] = makeResponse({})
|
|
134
154
|
|
|
135
|
-
//
|
|
155
|
+
// 2. Set this’s headers to a new Headers object with this’s relevant
|
|
136
156
|
// Realm, whose header list is this’s response’s header list and guard
|
|
137
157
|
// is "response".
|
|
138
158
|
this[kHeaders] = new Headers()
|
|
@@ -140,48 +160,20 @@ class Response {
|
|
|
140
160
|
this[kHeaders][kHeadersList] = this[kState].headersList
|
|
141
161
|
this[kHeaders][kRealm] = this[kRealm]
|
|
142
162
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
this[kState].status = init.status
|
|
146
|
-
}
|
|
163
|
+
// 3. Let bodyWithType be null.
|
|
164
|
+
let bodyWithType = null
|
|
147
165
|
|
|
148
|
-
//
|
|
149
|
-
if ('statusText' in init && init.statusText !== undefined) {
|
|
150
|
-
this[kState].statusText = String(init.statusText)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 7. If init["headers"] exists, then fill this’s headers with init["headers"].
|
|
154
|
-
if ('headers' in init) {
|
|
155
|
-
fill(this[kState].headersList, init.headers)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 8. If body is non-null, then:
|
|
166
|
+
// 4. If body is non-null, then set bodyWithType to the result of extracting body.
|
|
159
167
|
if (body != null) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
throw new TypeError('Response with null body status cannot have body')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// 2. Let Content-Type be null.
|
|
166
|
-
// 3. Set this’s response’s body and Content-Type to the result of
|
|
167
|
-
// extracting body.
|
|
168
|
-
const [extractedBody, contentType] = extractBody(body)
|
|
169
|
-
this[kState].body = extractedBody
|
|
170
|
-
|
|
171
|
-
// 4. If Content-Type is non-null and this’s response’s header list does
|
|
172
|
-
// not contain `Content-Type`, then append `Content-Type`/Content-Type
|
|
173
|
-
// to this’s response’s header list.
|
|
174
|
-
if (contentType && !this.headers.has('content-type')) {
|
|
175
|
-
this.headers.append('content-type', contentType)
|
|
176
|
-
}
|
|
168
|
+
const [extractedBody, type] = extractBody(body)
|
|
169
|
+
bodyWithType = { body: extractedBody, type }
|
|
177
170
|
}
|
|
171
|
+
|
|
172
|
+
// 5. Perform initialize a response given this, init, and bodyWithType.
|
|
173
|
+
initializeResponse(this, init, bodyWithType)
|
|
178
174
|
}
|
|
179
175
|
|
|
180
176
|
get [Symbol.toStringTag] () {
|
|
181
|
-
if (!(this instanceof Response)) {
|
|
182
|
-
throw new TypeError('Illegal invocation')
|
|
183
|
-
}
|
|
184
|
-
|
|
185
177
|
return this.constructor.name
|
|
186
178
|
}
|
|
187
179
|
|
|
@@ -477,6 +469,57 @@ function makeAppropriateNetworkError (fetchParams) {
|
|
|
477
469
|
: makeNetworkError(fetchParams.controller.terminated.reason)
|
|
478
470
|
}
|
|
479
471
|
|
|
472
|
+
// https://whatpr.org/fetch/1392.html#initialize-a-response
|
|
473
|
+
function initializeResponse (response, init, body) {
|
|
474
|
+
// 1. If init["status"] is not in the range 200 to 599, inclusive, then
|
|
475
|
+
// throw a RangeError.
|
|
476
|
+
if (init.status != null && (init.status < 200 || init.status > 599)) {
|
|
477
|
+
throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 2. If init["statusText"] does not match the reason-phrase token production,
|
|
481
|
+
// then throw a TypeError.
|
|
482
|
+
if ('statusText' in init && init.statusText != null) {
|
|
483
|
+
// See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
|
|
484
|
+
// reason-phrase = *( HTAB / SP / VCHAR / obs-text )
|
|
485
|
+
if (!isValidReasonPhrase(String(init.statusText))) {
|
|
486
|
+
throw new TypeError('Invalid statusText')
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 3. Set response’s response’s status to init["status"].
|
|
491
|
+
if ('status' in init && init.status != null) {
|
|
492
|
+
response[kState].status = init.status
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 4. Set response’s response’s status message to init["statusText"].
|
|
496
|
+
if ('statusText' in init && init.statusText != null) {
|
|
497
|
+
response[kState].statusText = init.statusText
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 5. If init["headers"] exists, then fill response’s headers with init["headers"].
|
|
501
|
+
if ('headers' in init && init.headers != null) {
|
|
502
|
+
fill(response[kState].headersList, init.headers)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 6. If body was given, then:
|
|
506
|
+
if (body) {
|
|
507
|
+
// 1. If response's status is a null body status, then throw a TypeError.
|
|
508
|
+
if (nullBodyStatus.includes(response.status)) {
|
|
509
|
+
throw new TypeError()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 2. Set response's body to body's body.
|
|
513
|
+
response[kState].body = body.body
|
|
514
|
+
|
|
515
|
+
// 3. If body's type is non-null and response's header list does not contain
|
|
516
|
+
// `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
|
|
517
|
+
if (body.type != null && !response[kState].headersList.has('Content-Type')) {
|
|
518
|
+
response[kState].headersList.append('content-type', body.type)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
480
523
|
module.exports = {
|
|
481
524
|
makeNetworkError,
|
|
482
525
|
makeResponse,
|
package/lib/fetch/util.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { redirectStatus } = require('./constants')
|
|
4
4
|
const { performance } = require('perf_hooks')
|
|
5
5
|
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
|
|
6
|
+
const assert = require('assert')
|
|
6
7
|
|
|
7
8
|
let File
|
|
8
9
|
|
|
@@ -316,47 +317,6 @@ function sameOrigin (A, B) {
|
|
|
316
317
|
return false
|
|
317
318
|
}
|
|
318
319
|
|
|
319
|
-
// https://fetch.spec.whatwg.org/#corb-check
|
|
320
|
-
function CORBCheck (request, response) {
|
|
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.
|
|
357
|
-
return 'allowed'
|
|
358
|
-
}
|
|
359
|
-
|
|
360
320
|
function createDeferredPromise () {
|
|
361
321
|
let res
|
|
362
322
|
let rej
|
|
@@ -384,6 +344,23 @@ function normalizeMethod (method) {
|
|
|
384
344
|
: method
|
|
385
345
|
}
|
|
386
346
|
|
|
347
|
+
// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
|
|
348
|
+
function serializeJavascriptValueToJSONString (value) {
|
|
349
|
+
// 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
|
|
350
|
+
const result = JSON.stringify(value)
|
|
351
|
+
|
|
352
|
+
// 2. If result is undefined, then throw a TypeError.
|
|
353
|
+
if (result === undefined) {
|
|
354
|
+
throw new TypeError('Value is not JSON serializable')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 3. Assert: result is a string.
|
|
358
|
+
assert(typeof result === 'string')
|
|
359
|
+
|
|
360
|
+
// 4. Return result.
|
|
361
|
+
return result
|
|
362
|
+
}
|
|
363
|
+
|
|
387
364
|
module.exports = {
|
|
388
365
|
isAborted,
|
|
389
366
|
isCancelled,
|
|
@@ -412,6 +389,6 @@ module.exports = {
|
|
|
412
389
|
isFileLike,
|
|
413
390
|
isValidReasonPhrase,
|
|
414
391
|
sameOrigin,
|
|
415
|
-
|
|
416
|
-
|
|
392
|
+
normalizeMethod,
|
|
393
|
+
serializeJavascriptValueToJSONString
|
|
417
394
|
}
|
package/lib/mock/mock-agent.js
CHANGED
|
@@ -96,6 +96,12 @@ class MockAgent extends Dispatcher {
|
|
|
96
96
|
this[kNetConnect] = false
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// This is required to bypass issues caused by using global symbols - see:
|
|
100
|
+
// https://github.com/nodejs/undici/issues/1447
|
|
101
|
+
get isMockActive () {
|
|
102
|
+
return this[kIsMockActive]
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
[kMockAgentSet] (origin, dispatcher) {
|
|
100
106
|
this[kClients].set(origin, new FakeWeakRef(dispatcher))
|
|
101
107
|
}
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -6,7 +6,6 @@ const {
|
|
|
6
6
|
kMockAgent,
|
|
7
7
|
kOriginalDispatch,
|
|
8
8
|
kOrigin,
|
|
9
|
-
kIsMockActive,
|
|
10
9
|
kGetNetConnect
|
|
11
10
|
} = require('./mock-symbols')
|
|
12
11
|
|
|
@@ -302,7 +301,7 @@ function buildMockDispatch () {
|
|
|
302
301
|
const originalDispatch = this[kOriginalDispatch]
|
|
303
302
|
|
|
304
303
|
return function dispatch (opts, handler) {
|
|
305
|
-
if (agent
|
|
304
|
+
if (agent.isMockActive) {
|
|
306
305
|
try {
|
|
307
306
|
mockDispatch.call(this, opts, handler)
|
|
308
307
|
} catch (error) {
|
package/lib/proxy-agent.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"lint:fix": "standard --fix | snazzy",
|
|
49
49
|
"test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:jest && tsd",
|
|
50
50
|
"test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch",
|
|
51
|
-
"test:fetch": "node scripts/verifyVersion.js 16 || tap test/fetch/*.js",
|
|
51
|
+
"test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js)",
|
|
52
52
|
"test:jest": "jest",
|
|
53
53
|
"test:tap": "tap test/*.js test/diagnostics-channel/*.js",
|
|
54
54
|
"test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"sinon": "^14.0.0",
|
|
92
92
|
"snazzy": "^9.0.0",
|
|
93
93
|
"standard": "^17.0.0",
|
|
94
|
+
"table": "^6.8.0",
|
|
94
95
|
"tap": "^16.1.0",
|
|
95
96
|
"tsd": "^0.20.0",
|
|
96
97
|
"wait-on": "^6.0.0"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Socket } from "net";
|
|
2
|
+
import { connector } from "./connector";
|
|
3
|
+
import { HttpMethod } from "./dispatcher";
|
|
4
|
+
|
|
5
|
+
declare namespace DiagnosticsChannel {
|
|
6
|
+
interface Request {
|
|
7
|
+
origin?: string | URL;
|
|
8
|
+
completed: boolean;
|
|
9
|
+
method?: HttpMethod;
|
|
10
|
+
path: string;
|
|
11
|
+
headers: string;
|
|
12
|
+
addHeader(key: string, value: string): Request;
|
|
13
|
+
}
|
|
14
|
+
interface Response {
|
|
15
|
+
statusCode: number;
|
|
16
|
+
statusText: string;
|
|
17
|
+
headers: Array<Buffer>;
|
|
18
|
+
}
|
|
19
|
+
type Error = unknown;
|
|
20
|
+
interface ConnectParams {
|
|
21
|
+
host: URL["host"];
|
|
22
|
+
hostname: URL["hostname"];
|
|
23
|
+
protocol: URL["protocol"];
|
|
24
|
+
port: URL["port"];
|
|
25
|
+
servername: string | null;
|
|
26
|
+
}
|
|
27
|
+
type Connector = typeof connector;
|
|
28
|
+
export interface RequestCreateMessage {
|
|
29
|
+
request: Request;
|
|
30
|
+
}
|
|
31
|
+
export interface RequestBodySentMessage {
|
|
32
|
+
request: Request;
|
|
33
|
+
}
|
|
34
|
+
export interface RequestHeadersMessage {
|
|
35
|
+
request: Request;
|
|
36
|
+
response: Response;
|
|
37
|
+
}
|
|
38
|
+
export interface RequestTrailersMessage {
|
|
39
|
+
request: Request;
|
|
40
|
+
trailers: Array<Buffer>;
|
|
41
|
+
}
|
|
42
|
+
export interface RequestErrorMessage {
|
|
43
|
+
request: Request;
|
|
44
|
+
error: Error;
|
|
45
|
+
}
|
|
46
|
+
export interface ClientSendHeadersMessage {
|
|
47
|
+
request: Request;
|
|
48
|
+
headers: string;
|
|
49
|
+
socket: Socket;
|
|
50
|
+
}
|
|
51
|
+
export interface ClientBeforeConnectMessage {
|
|
52
|
+
connectParams: ConnectParams;
|
|
53
|
+
connector: Connector;
|
|
54
|
+
}
|
|
55
|
+
export interface ClientConnectedMessage {
|
|
56
|
+
socket: Socket;
|
|
57
|
+
connectParams: ConnectParams;
|
|
58
|
+
connector: Connector;
|
|
59
|
+
}
|
|
60
|
+
export interface ClientConnectErrorMessage {
|
|
61
|
+
error: Error;
|
|
62
|
+
socket: Socket;
|
|
63
|
+
connectParams: ConnectParams;
|
|
64
|
+
connector: Connector;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/types/dispatcher.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ declare namespace Dispatcher {
|
|
|
47
47
|
body?: string | Buffer | Uint8Array | Readable | null | FormData;
|
|
48
48
|
/** Default: `null` */
|
|
49
49
|
headers?: IncomingHttpHeaders | string[] | null;
|
|
50
|
+
/** Query string params to be embedded in the request URL. Default: `null` */
|
|
51
|
+
query?: Record<string, any>;
|
|
50
52
|
/** 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`. */
|
|
51
53
|
idempotent?: boolean;
|
|
52
54
|
/** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */
|
|
@@ -55,6 +57,8 @@ declare namespace Dispatcher {
|
|
|
55
57
|
headersTimeout?: number | null;
|
|
56
58
|
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use 0 to disable it entirely. Defaults to 30 seconds. */
|
|
57
59
|
bodyTimeout?: number | null;
|
|
60
|
+
/** Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. Defaults to false */
|
|
61
|
+
throwOnError?: boolean;
|
|
58
62
|
}
|
|
59
63
|
export interface ConnectOptions {
|
|
60
64
|
path: string;
|
package/types/fetch.d.ts
CHANGED
|
@@ -101,19 +101,19 @@ type RequestDestination =
|
|
|
101
101
|
| 'xslt'
|
|
102
102
|
|
|
103
103
|
export interface RequestInit {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
method?: string
|
|
105
|
+
keepalive?: boolean
|
|
106
|
+
headers?: HeadersInit
|
|
107
|
+
body?: BodyInit
|
|
108
|
+
redirect?: RequestRedirect
|
|
109
|
+
integrity?: string
|
|
110
|
+
signal?: AbortSignal
|
|
111
|
+
credentials?: RequestCredentials
|
|
112
|
+
mode?: RequestMode
|
|
113
|
+
referrer?: string
|
|
114
|
+
referrerPolicy?: ReferrerPolicy
|
|
115
|
+
window?: null
|
|
116
|
+
dispatcher?: Dispatcher
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export type ReferrerPolicy =
|
|
@@ -199,5 +199,6 @@ export declare class Response implements BodyMixin {
|
|
|
199
199
|
readonly clone: () => Response
|
|
200
200
|
|
|
201
201
|
static error (): Response
|
|
202
|
+
static json(data: any, init?: ResponseInit): Response
|
|
202
203
|
static redirect (url: string | URL, status: ResponseRedirectStatus): Response
|
|
203
204
|
}
|