undici 5.2.0 → 5.5.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 +22 -17
- package/docs/api/Dispatcher.md +3 -1
- package/docs/api/MockPool.md +1 -0
- 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/client.js +5 -0
- package/lib/core/errors.js +14 -0
- package/lib/core/request.js +11 -8
- package/lib/core/util.js +47 -1
- package/lib/fetch/body.js +2 -2
- package/lib/fetch/constants.js +0 -31
- package/lib/fetch/file.js +0 -8
- package/lib/fetch/formdata.js +59 -24
- package/lib/fetch/headers.js +12 -68
- package/lib/fetch/index.js +4 -16
- package/lib/fetch/request.js +5 -10
- package/lib/fetch/response.js +112 -94
- package/lib/fetch/util.js +48 -43
- package/lib/mock/mock-agent.js +6 -0
- package/lib/mock/mock-interceptor.js +8 -3
- package/lib/mock/mock-utils.js +9 -6
- package/lib/proxy-agent.js +0 -1
- package/package.json +6 -5
- package/types/connector.d.ts +4 -5
- package/types/diagnostics-channel.d.ts +66 -0
- package/types/dispatcher.d.ts +5 -1
- package/types/fetch.d.ts +24 -23
- package/types/formdata.d.ts +15 -11
- package/types/mock-interceptor.d.ts +3 -1
package/README.md
CHANGED
|
@@ -185,20 +185,19 @@ Help us improve the test coverage by following instructions at [nodejs/undici/#9
|
|
|
185
185
|
Basic usage example:
|
|
186
186
|
|
|
187
187
|
```js
|
|
188
|
-
|
|
188
|
+
import { fetch } from 'undici';
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
190
|
+
|
|
191
|
+
const res = await fetch('https://example.com')
|
|
192
|
+
const json = await res.json()
|
|
193
|
+
console.log(json);
|
|
195
194
|
```
|
|
196
195
|
|
|
197
196
|
You can pass an optional dispatcher to `fetch` as:
|
|
198
197
|
|
|
199
198
|
```js
|
|
200
199
|
import { fetch, Agent } from 'undici'
|
|
201
|
-
|
|
200
|
+
|
|
202
201
|
const res = await fetch('https://example.com', {
|
|
203
202
|
// Mocks are also supported
|
|
204
203
|
dispatcher: new Agent({
|
|
@@ -235,9 +234,7 @@ const data = {
|
|
|
235
234
|
},
|
|
236
235
|
};
|
|
237
236
|
|
|
238
|
-
|
|
239
|
-
await fetch("https://example.com", { body: data, method: 'POST' });
|
|
240
|
-
})();
|
|
237
|
+
await fetch("https://example.com", { body: data, method: 'POST' });
|
|
241
238
|
```
|
|
242
239
|
|
|
243
240
|
#### `response.body`
|
|
@@ -245,14 +242,12 @@ const data = {
|
|
|
245
242
|
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()`.
|
|
246
243
|
|
|
247
244
|
```js
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
import { fetch } from 'undici';
|
|
246
|
+
import { Readable } from 'node:stream';
|
|
250
247
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const readableNodeStream = Readable.fromWeb(readableWebStream);
|
|
255
|
-
}
|
|
248
|
+
const response = await fetch('https://example.com')
|
|
249
|
+
const readableWebStream = response.body;
|
|
250
|
+
const readableNodeStream = Readable.fromWeb(readableWebStream);
|
|
256
251
|
```
|
|
257
252
|
|
|
258
253
|
#### Specification Compliance
|
|
@@ -288,6 +283,15 @@ const headers = await fetch(url)
|
|
|
288
283
|
.then(res => res.headers)
|
|
289
284
|
```
|
|
290
285
|
|
|
286
|
+
##### Forbidden and Safelisted Header Names
|
|
287
|
+
|
|
288
|
+
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
|
|
289
|
+
* https://fetch.spec.whatwg.org/#forbidden-header-name
|
|
290
|
+
* https://fetch.spec.whatwg.org/#forbidden-response-header-name
|
|
291
|
+
* https://github.com/wintercg/fetch/issues/6
|
|
292
|
+
|
|
293
|
+
The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
|
|
294
|
+
|
|
291
295
|
### `undici.upgrade([url, options]): Promise`
|
|
292
296
|
|
|
293
297
|
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
|
|
@@ -375,6 +379,7 @@ Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
|
|
|
375
379
|
* [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
|
|
376
380
|
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
|
|
377
381
|
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
|
|
382
|
+
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
|
|
378
383
|
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
|
|
379
384
|
* [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
|
|
380
385
|
* [__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.
|
package/docs/api/MockPool.md
CHANGED
|
@@ -57,6 +57,7 @@ Returns: `MockInterceptor` corresponding to the input options.
|
|
|
57
57
|
* **method** `string | RegExp | (method: string) => boolean` - a matcher for the HTTP request method.
|
|
58
58
|
* **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
|
|
59
59
|
* **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
|
|
60
|
+
* **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params.
|
|
60
61
|
|
|
61
62
|
### Return: `MockInterceptor`
|
|
62
63
|
|
|
@@ -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/client.js
CHANGED
|
@@ -873,6 +873,11 @@ class Parser {
|
|
|
873
873
|
// have been queued since then.
|
|
874
874
|
util.destroy(socket, new InformationalError('reset'))
|
|
875
875
|
return constants.ERROR.PAUSED
|
|
876
|
+
} else if (client[kPipelining] === 1) {
|
|
877
|
+
// We must wait a full event loop cycle to reuse this socket to make sure
|
|
878
|
+
// that non-spec compliant servers are not closing the connection even if they
|
|
879
|
+
// said they won't.
|
|
880
|
+
setImmediate(resume, client)
|
|
876
881
|
} else {
|
|
877
882
|
resume(client)
|
|
878
883
|
}
|
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,19 +72,20 @@ 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) {
|
|
76
80
|
this.body = null
|
|
77
81
|
} else if (util.isStream(body)) {
|
|
78
82
|
this.body = body
|
|
79
|
-
} else if (body instanceof DataView) {
|
|
80
|
-
// TODO: Why is DataView special?
|
|
81
|
-
this.body = body.buffer.byteLength ? Buffer.from(body.buffer) : null
|
|
82
|
-
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
83
|
-
this.body = body.byteLength ? Buffer.from(body) : null
|
|
84
83
|
} else if (util.isBuffer(body)) {
|
|
85
84
|
this.body = body.byteLength ? body : null
|
|
85
|
+
} else if (ArrayBuffer.isView(body)) {
|
|
86
|
+
this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
|
|
87
|
+
} else if (body instanceof ArrayBuffer) {
|
|
88
|
+
this.body = body.byteLength ? Buffer.from(body) : null
|
|
86
89
|
} else if (typeof body === 'string') {
|
|
87
90
|
this.body = body.length ? Buffer.from(body) : null
|
|
88
91
|
} else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
|
|
@@ -97,7 +100,7 @@ class Request {
|
|
|
97
100
|
|
|
98
101
|
this.upgrade = upgrade || null
|
|
99
102
|
|
|
100
|
-
this.path = path
|
|
103
|
+
this.path = query ? util.buildURL(path, query) : path
|
|
101
104
|
|
|
102
105
|
this.origin = origin
|
|
103
106
|
|
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/body.js
CHANGED
|
@@ -9,7 +9,7 @@ const { kBodyUsed } = require('../core/symbols')
|
|
|
9
9
|
const assert = require('assert')
|
|
10
10
|
const { NotSupportedError } = require('../core/errors')
|
|
11
11
|
const { isErrored } = require('../core/util')
|
|
12
|
-
const { isUint8Array } = require('util/types')
|
|
12
|
+
const { isUint8Array, isArrayBuffer } = require('util/types')
|
|
13
13
|
|
|
14
14
|
let ReadableStream
|
|
15
15
|
|
|
@@ -61,7 +61,7 @@ function extractBody (object, keepalive = false) {
|
|
|
61
61
|
|
|
62
62
|
// Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`.
|
|
63
63
|
contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
64
|
-
} else if (object
|
|
64
|
+
} else if (isArrayBuffer(object) || ArrayBuffer.isView(object)) {
|
|
65
65
|
// BufferSource
|
|
66
66
|
|
|
67
67
|
if (object instanceof DataView) {
|
package/lib/fetch/constants.js
CHANGED
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const forbiddenHeaderNames = [
|
|
4
|
-
'accept-charset',
|
|
5
|
-
'accept-encoding',
|
|
6
|
-
'access-control-request-headers',
|
|
7
|
-
'access-control-request-method',
|
|
8
|
-
'connection',
|
|
9
|
-
'content-length',
|
|
10
|
-
'cookie',
|
|
11
|
-
'cookie2',
|
|
12
|
-
'date',
|
|
13
|
-
'dnt',
|
|
14
|
-
'expect',
|
|
15
|
-
'host',
|
|
16
|
-
'keep-alive',
|
|
17
|
-
'origin',
|
|
18
|
-
'referer',
|
|
19
|
-
'te',
|
|
20
|
-
'trailer',
|
|
21
|
-
'transfer-encoding',
|
|
22
|
-
'upgrade',
|
|
23
|
-
'via'
|
|
24
|
-
]
|
|
25
|
-
|
|
26
3
|
const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
|
|
27
4
|
|
|
28
5
|
const nullBodyStatus = [101, 204, 205, 304]
|
|
@@ -58,9 +35,6 @@ const requestCache = [
|
|
|
58
35
|
'only-if-cached'
|
|
59
36
|
]
|
|
60
37
|
|
|
61
|
-
// https://fetch.spec.whatwg.org/#forbidden-response-header-name
|
|
62
|
-
const forbiddenResponseHeaderNames = ['set-cookie', 'set-cookie2']
|
|
63
|
-
|
|
64
38
|
const requestBodyHeader = [
|
|
65
39
|
'content-encoding',
|
|
66
40
|
'content-language',
|
|
@@ -86,12 +60,8 @@ const subresource = [
|
|
|
86
60
|
''
|
|
87
61
|
]
|
|
88
62
|
|
|
89
|
-
const corsSafeListedResponseHeaderNames = [] // TODO
|
|
90
|
-
|
|
91
63
|
module.exports = {
|
|
92
64
|
subresource,
|
|
93
|
-
forbiddenResponseHeaderNames,
|
|
94
|
-
corsSafeListedResponseHeaderNames,
|
|
95
65
|
forbiddenMethods,
|
|
96
66
|
requestBodyHeader,
|
|
97
67
|
referrerPolicy,
|
|
@@ -99,7 +69,6 @@ module.exports = {
|
|
|
99
69
|
requestMode,
|
|
100
70
|
requestCredentials,
|
|
101
71
|
requestCache,
|
|
102
|
-
forbiddenHeaderNames,
|
|
103
72
|
redirectStatus,
|
|
104
73
|
corsSafeListedMethods,
|
|
105
74
|
nullBodyStatus,
|
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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { isBlobLike, isFileLike, toUSVString } = require('./util')
|
|
3
|
+
const { isBlobLike, isFileLike, toUSVString, makeIterator } = require('./util')
|
|
4
4
|
const { kState } = require('./symbols')
|
|
5
5
|
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,52 +184,71 @@ class FormData {
|
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
get [Symbol.toStringTag] () {
|
|
187
|
+
return this.constructor.name
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
entries () {
|
|
185
191
|
if (!(this instanceof FormData)) {
|
|
186
192
|
throw new TypeError('Illegal invocation')
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
return
|
|
195
|
+
return makeIterator(
|
|
196
|
+
makeIterable(this[kState], 'entries'),
|
|
197
|
+
'FormData'
|
|
198
|
+
)
|
|
190
199
|
}
|
|
191
200
|
|
|
192
|
-
|
|
201
|
+
keys () {
|
|
193
202
|
if (!(this instanceof FormData)) {
|
|
194
203
|
throw new TypeError('Illegal invocation')
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
206
|
+
return makeIterator(
|
|
207
|
+
makeIterable(this[kState], 'keys'),
|
|
208
|
+
'FormData'
|
|
209
|
+
)
|
|
200
210
|
}
|
|
201
211
|
|
|
202
|
-
|
|
212
|
+
values () {
|
|
203
213
|
if (!(this instanceof FormData)) {
|
|
204
214
|
throw new TypeError('Illegal invocation')
|
|
205
215
|
}
|
|
206
216
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
return makeIterator(
|
|
218
|
+
makeIterable(this[kState], 'values'),
|
|
219
|
+
'FormData'
|
|
220
|
+
)
|
|
210
221
|
}
|
|
211
222
|
|
|
212
|
-
|
|
223
|
+
/**
|
|
224
|
+
* @param {(value: string, key: string, self: FormData) => void} callbackFn
|
|
225
|
+
* @param {unknown} thisArg
|
|
226
|
+
*/
|
|
227
|
+
forEach (callbackFn, thisArg = globalThis) {
|
|
213
228
|
if (!(this instanceof FormData)) {
|
|
214
229
|
throw new TypeError('Illegal invocation')
|
|
215
230
|
}
|
|
216
231
|
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
if (arguments.length < 1) {
|
|
233
|
+
throw new TypeError(
|
|
234
|
+
`Failed to execute 'forEach' on 'FormData': 1 argument required, but only ${arguments.length} present.`
|
|
235
|
+
)
|
|
219
236
|
}
|
|
220
|
-
}
|
|
221
237
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
if (typeof callbackFn !== 'function') {
|
|
239
|
+
throw new TypeError(
|
|
240
|
+
"Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'."
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const [key, value] of this) {
|
|
245
|
+
callbackFn.apply(thisArg, [value, key, this])
|
|
227
246
|
}
|
|
228
247
|
}
|
|
229
248
|
}
|
|
230
249
|
|
|
250
|
+
FormData.prototype[Symbol.iterator] = FormData.prototype.entries
|
|
251
|
+
|
|
231
252
|
function makeEntry (name, value, filename) {
|
|
232
253
|
// To create an entry for name, value, and optionally a filename, run these
|
|
233
254
|
// steps:
|
|
@@ -245,8 +266,8 @@ function makeEntry (name, value, filename) {
|
|
|
245
266
|
// object, representing the same bytes, whose name attribute value is "blob".
|
|
246
267
|
if (isBlobLike(value) && !isFileLike(value)) {
|
|
247
268
|
value = value instanceof Blob
|
|
248
|
-
? new File([value], 'blob')
|
|
249
|
-
: new FileLike(value, 'blob')
|
|
269
|
+
? new File([value], 'blob', value)
|
|
270
|
+
: new FileLike(value, 'blob', value)
|
|
250
271
|
}
|
|
251
272
|
|
|
252
273
|
// 4. If value is (now) a File object and filename is given, then set value to a
|
|
@@ -258,8 +279,8 @@ function makeEntry (name, value, filename) {
|
|
|
258
279
|
// creating one more File instance doesn't make much sense....
|
|
259
280
|
if (isFileLike(value) && filename != null) {
|
|
260
281
|
value = value instanceof File
|
|
261
|
-
? new File([value], filename)
|
|
262
|
-
: new FileLike(value, filename)
|
|
282
|
+
? new File([value], filename, value)
|
|
283
|
+
: new FileLike(value, filename, value)
|
|
263
284
|
}
|
|
264
285
|
|
|
265
286
|
// 5. Set entry’s value to value.
|
|
@@ -269,4 +290,18 @@ function makeEntry (name, value, filename) {
|
|
|
269
290
|
return entry
|
|
270
291
|
}
|
|
271
292
|
|
|
272
|
-
|
|
293
|
+
function * makeIterable (entries, type) {
|
|
294
|
+
// The value pairs to iterate over are this’s entry list’s entries
|
|
295
|
+
// with the key being the name and the value being the value.
|
|
296
|
+
for (const { name, value } of entries) {
|
|
297
|
+
if (type === 'entries') {
|
|
298
|
+
yield [name, value]
|
|
299
|
+
} else if (type === 'values') {
|
|
300
|
+
yield value
|
|
301
|
+
} else {
|
|
302
|
+
yield name
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { FormData }
|