undici 5.4.0 → 5.6.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 +11 -16
- package/docs/api/Dispatcher.md +6 -4
- package/lib/api/api-request.js +31 -3
- package/lib/client.js +3 -5
- package/lib/core/connect.js +3 -1
- package/lib/core/errors.js +2 -10
- package/lib/core/request.js +9 -6
- package/lib/fetch/body.js +97 -61
- package/lib/fetch/constants.js +12 -0
- package/lib/fetch/file.js +131 -11
- package/lib/fetch/formdata.js +137 -86
- package/lib/fetch/headers.js +228 -110
- package/lib/fetch/index.js +30 -27
- package/lib/fetch/request.js +119 -18
- package/lib/fetch/response.js +111 -35
- package/lib/fetch/util.js +74 -1
- package/lib/fetch/webidl.js +594 -0
- package/lib/proxy-agent.js +94 -22
- package/package.json +3 -3
- package/types/connector.d.ts +4 -5
- package/types/diagnostics-channel.d.ts +1 -0
- package/types/errors.d.ts +11 -0
- package/types/fetch.d.ts +10 -10
- package/types/file.d.ts +8 -3
- package/types/formdata.d.ts +15 -11
- package/types/proxy-agent.d.ts +4 -0
package/README.md
CHANGED
|
@@ -185,13 +185,12 @@ 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:
|
|
@@ -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
|
package/docs/api/Dispatcher.md
CHANGED
|
@@ -461,14 +461,14 @@ Arguments:
|
|
|
461
461
|
* **options** `RequestOptions`
|
|
462
462
|
* **callback** `(error: Error | null, data: ResponseData) => void` (optional)
|
|
463
463
|
|
|
464
|
-
Returns: `void | Promise<ResponseData>` - Only returns a `Promise` if no `callback` argument was passed
|
|
464
|
+
Returns: `void | Promise<ResponseData>` - Only returns a `Promise` if no `callback` argument was passed.
|
|
465
465
|
|
|
466
466
|
#### Parameter: `RequestOptions`
|
|
467
467
|
|
|
468
468
|
Extends: [`DispatchOptions`](#parameter-dispatchoptions)
|
|
469
469
|
|
|
470
|
-
* **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData
|
|
471
|
-
* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null
|
|
470
|
+
* **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`.
|
|
471
|
+
* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`.
|
|
472
472
|
* **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
|
|
473
473
|
|
|
474
474
|
The `RequestOptions.method` property should not be value `'CONNECT'`.
|
|
@@ -476,7 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
|
|
|
476
476
|
#### Parameter: `ResponseData`
|
|
477
477
|
|
|
478
478
|
* **statusCode** `number`
|
|
479
|
-
* **headers** `http.IncomingHttpHeaders`
|
|
479
|
+
* **headers** `http.IncomingHttpHeaders` - Note that all header keys are lower-cased, e. g. `content-type`.
|
|
480
480
|
* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
|
|
481
481
|
* **trailers** `Record<string, string>` - This object starts out
|
|
482
482
|
as empty and will be mutated to contain trailers after `body` has emitted `'end'`.
|
|
@@ -497,6 +497,8 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
|
|
|
497
497
|
|
|
498
498
|
- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144.
|
|
499
499
|
|
|
500
|
+
Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`.
|
|
501
|
+
|
|
500
502
|
#### Example 1 - Basic GET Request
|
|
501
503
|
|
|
502
504
|
```js
|
package/lib/api/api-request.js
CHANGED
|
@@ -84,7 +84,8 @@ class RequestHandler extends AsyncResource {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const parsedHeaders = util.parseHeaders(rawHeaders)
|
|
87
|
-
const
|
|
87
|
+
const contentType = parsedHeaders['content-type']
|
|
88
|
+
const body = new Readable(resume, abort, contentType)
|
|
88
89
|
|
|
89
90
|
this.callback = null
|
|
90
91
|
this.res = body
|
|
@@ -92,8 +93,8 @@ class RequestHandler extends AsyncResource {
|
|
|
92
93
|
|
|
93
94
|
if (callback !== null) {
|
|
94
95
|
if (this.throwOnError && statusCode >= 400) {
|
|
95
|
-
this.runInAsyncScope(
|
|
96
|
-
|
|
96
|
+
this.runInAsyncScope(getResolveErrorBodyCallback, null,
|
|
97
|
+
{ callback, body, contentType, statusCode, statusMessage, headers }
|
|
97
98
|
)
|
|
98
99
|
return
|
|
99
100
|
}
|
|
@@ -152,6 +153,33 @@ class RequestHandler extends AsyncResource {
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) {
|
|
157
|
+
if (statusCode === 204 || !contentType) {
|
|
158
|
+
body.dump()
|
|
159
|
+
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (contentType.startsWith('application/json')) {
|
|
165
|
+
const payload = await body.json()
|
|
166
|
+
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (contentType.startsWith('text/')) {
|
|
171
|
+
const payload = await body.text()
|
|
172
|
+
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Process in a fallback if error
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
body.dump()
|
|
180
|
+
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
|
|
181
|
+
}
|
|
182
|
+
|
|
155
183
|
function request (opts, callback) {
|
|
156
184
|
if (callback === undefined) {
|
|
157
185
|
return new Promise((resolve, reject) => {
|
package/lib/client.js
CHANGED
|
@@ -720,7 +720,7 @@ class Parser {
|
|
|
720
720
|
}
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
-
if (request.method === 'CONNECT'
|
|
723
|
+
if (request.method === 'CONNECT') {
|
|
724
724
|
assert(client[kRunning] === 1)
|
|
725
725
|
this.upgrade = true
|
|
726
726
|
return 2
|
|
@@ -889,10 +889,8 @@ function onParserTimeout (parser) {
|
|
|
889
889
|
|
|
890
890
|
/* istanbul ignore else */
|
|
891
891
|
if (timeoutType === TIMEOUT_HEADERS) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
util.destroy(socket, new HeadersTimeoutError())
|
|
895
|
-
}
|
|
892
|
+
assert(!parser.paused, 'cannot be paused while waiting for headers')
|
|
893
|
+
util.destroy(socket, new HeadersTimeoutError())
|
|
896
894
|
} else if (timeoutType === TIMEOUT_BODY) {
|
|
897
895
|
if (!parser.paused) {
|
|
898
896
|
util.destroy(socket, new BodyTimeoutError())
|
package/lib/core/connect.js
CHANGED
|
@@ -21,7 +21,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
21
21
|
timeout = timeout == null ? 10e3 : timeout
|
|
22
22
|
maxCachedSessions = maxCachedSessions == null ? 100 : maxCachedSessions
|
|
23
23
|
|
|
24
|
-
return function connect ({ hostname, host, protocol, port, servername }, callback) {
|
|
24
|
+
return function connect ({ hostname, host, protocol, port, servername, httpSocket }, callback) {
|
|
25
25
|
let socket
|
|
26
26
|
if (protocol === 'https:') {
|
|
27
27
|
if (!tls) {
|
|
@@ -39,6 +39,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
39
39
|
...options,
|
|
40
40
|
servername,
|
|
41
41
|
session,
|
|
42
|
+
socket: httpSocket, // upgrade socket connection
|
|
42
43
|
port: port || 443,
|
|
43
44
|
host: hostname
|
|
44
45
|
})
|
|
@@ -65,6 +66,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
65
66
|
}
|
|
66
67
|
})
|
|
67
68
|
} else {
|
|
69
|
+
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
|
|
68
70
|
socket = net.connect({
|
|
69
71
|
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
|
|
70
72
|
...options,
|
package/lib/core/errors.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
class AbortError extends Error {
|
|
4
|
-
constructor () {
|
|
5
|
-
super('The operation was aborted')
|
|
6
|
-
this.code = 'ABORT_ERR'
|
|
7
|
-
this.name = 'AbortError'
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
3
|
class UndiciError extends Error {
|
|
12
4
|
constructor (message) {
|
|
13
5
|
super(message)
|
|
@@ -57,12 +49,13 @@ class BodyTimeoutError extends UndiciError {
|
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
class ResponseStatusCodeError extends UndiciError {
|
|
60
|
-
constructor (message, statusCode, headers) {
|
|
52
|
+
constructor (message, statusCode, headers, body) {
|
|
61
53
|
super(message)
|
|
62
54
|
Error.captureStackTrace(this, ResponseStatusCodeError)
|
|
63
55
|
this.name = 'ResponseStatusCodeError'
|
|
64
56
|
this.message = message || 'Response Status Code Error'
|
|
65
57
|
this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
|
|
58
|
+
this.body = body
|
|
66
59
|
this.status = statusCode
|
|
67
60
|
this.statusCode = statusCode
|
|
68
61
|
this.headers = headers
|
|
@@ -191,7 +184,6 @@ class HTTPParserError extends Error {
|
|
|
191
184
|
}
|
|
192
185
|
|
|
193
186
|
module.exports = {
|
|
194
|
-
AbortError,
|
|
195
187
|
HTTPParserError,
|
|
196
188
|
UndiciError,
|
|
197
189
|
HeadersTimeoutError,
|
package/lib/core/request.js
CHANGED
|
@@ -48,7 +48,11 @@ class Request {
|
|
|
48
48
|
}, handler) {
|
|
49
49
|
if (typeof path !== 'string') {
|
|
50
50
|
throw new InvalidArgumentError('path must be a string')
|
|
51
|
-
} else if (
|
|
51
|
+
} else if (
|
|
52
|
+
path[0] !== '/' &&
|
|
53
|
+
!(path.startsWith('http://') || path.startsWith('https://')) &&
|
|
54
|
+
method !== 'CONNECT'
|
|
55
|
+
) {
|
|
52
56
|
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
|
|
53
57
|
}
|
|
54
58
|
|
|
@@ -80,13 +84,12 @@ class Request {
|
|
|
80
84
|
this.body = null
|
|
81
85
|
} else if (util.isStream(body)) {
|
|
82
86
|
this.body = body
|
|
83
|
-
} else if (body instanceof DataView) {
|
|
84
|
-
// TODO: Why is DataView special?
|
|
85
|
-
this.body = body.buffer.byteLength ? Buffer.from(body.buffer) : null
|
|
86
|
-
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
87
|
-
this.body = body.byteLength ? Buffer.from(body) : null
|
|
88
87
|
} else if (util.isBuffer(body)) {
|
|
89
88
|
this.body = body.byteLength ? body : null
|
|
89
|
+
} else if (ArrayBuffer.isView(body)) {
|
|
90
|
+
this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
|
|
91
|
+
} else if (body instanceof ArrayBuffer) {
|
|
92
|
+
this.body = body.byteLength ? Buffer.from(body) : null
|
|
90
93
|
} else if (typeof body === 'string') {
|
|
91
94
|
this.body = body.length ? Buffer.from(body) : null
|
|
92
95
|
} else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
|
package/lib/fetch/body.js
CHANGED
|
@@ -4,12 +4,13 @@ const util = require('../core/util')
|
|
|
4
4
|
const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util')
|
|
5
5
|
const { FormData } = require('./formdata')
|
|
6
6
|
const { kState } = require('./symbols')
|
|
7
|
+
const { webidl } = require('./webidl')
|
|
7
8
|
const { Blob } = require('buffer')
|
|
8
9
|
const { kBodyUsed } = require('../core/symbols')
|
|
9
10
|
const assert = require('assert')
|
|
10
11
|
const { NotSupportedError } = require('../core/errors')
|
|
11
12
|
const { isErrored } = require('../core/util')
|
|
12
|
-
const { isUint8Array } = require('util/types')
|
|
13
|
+
const { isUint8Array, isArrayBuffer } = require('util/types')
|
|
13
14
|
|
|
14
15
|
let ReadableStream
|
|
15
16
|
|
|
@@ -61,7 +62,7 @@ function extractBody (object, keepalive = false) {
|
|
|
61
62
|
|
|
62
63
|
// Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`.
|
|
63
64
|
contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
64
|
-
} else if (object
|
|
65
|
+
} else if (isArrayBuffer(object) || ArrayBuffer.isView(object)) {
|
|
65
66
|
// BufferSource
|
|
66
67
|
|
|
67
68
|
if (object instanceof DataView) {
|
|
@@ -262,100 +263,135 @@ function cloneBody (body) {
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
function bodyMixinMethods (instance) {
|
|
267
|
+
const methods = {
|
|
268
|
+
async blob () {
|
|
269
|
+
if (!(this instanceof instance)) {
|
|
270
|
+
throw new TypeError('Illegal invocation')
|
|
271
|
+
}
|
|
268
272
|
|
|
269
|
-
|
|
270
|
-
if (isUint8Array(this[kState].body)) {
|
|
271
|
-
chunks.push(this[kState].body)
|
|
272
|
-
} else {
|
|
273
|
-
const stream = this[kState].body.stream
|
|
273
|
+
const chunks = []
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
if (this[kState].body) {
|
|
276
|
+
if (isUint8Array(this[kState].body)) {
|
|
277
|
+
chunks.push(this[kState].body)
|
|
278
|
+
} else {
|
|
279
|
+
const stream = this[kState].body.stream
|
|
278
280
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
if (util.isDisturbed(stream)) {
|
|
282
|
+
throw new TypeError('disturbed')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (stream.locked) {
|
|
286
|
+
throw new TypeError('locked')
|
|
287
|
+
}
|
|
282
288
|
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
// Compat.
|
|
290
|
+
stream[kBodyUsed] = true
|
|
285
291
|
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
for await (const chunk of stream) {
|
|
293
|
+
chunks.push(chunk)
|
|
294
|
+
}
|
|
288
295
|
}
|
|
289
296
|
}
|
|
290
|
-
}
|
|
291
297
|
|
|
292
|
-
|
|
293
|
-
|
|
298
|
+
return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
|
|
299
|
+
},
|
|
294
300
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
async arrayBuffer () {
|
|
302
|
+
if (!(this instanceof instance)) {
|
|
303
|
+
throw new TypeError('Illegal invocation')
|
|
304
|
+
}
|
|
299
305
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
},
|
|
306
|
+
const blob = await this.blob()
|
|
307
|
+
return await blob.arrayBuffer()
|
|
308
|
+
},
|
|
304
309
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
async text () {
|
|
311
|
+
if (!(this instanceof instance)) {
|
|
312
|
+
throw new TypeError('Illegal invocation')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const blob = await this.blob()
|
|
316
|
+
return toUSVString(await blob.text())
|
|
317
|
+
},
|
|
308
318
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// If mimeType’s essence is "multipart/form-data", then:
|
|
313
|
-
if (/multipart\/form-data/.test(contentType)) {
|
|
314
|
-
throw new NotSupportedError('multipart/form-data not supported')
|
|
315
|
-
} else if (/application\/x-www-form-urlencoded/.test(contentType)) {
|
|
316
|
-
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
|
|
317
|
-
|
|
318
|
-
// 1. Let entries be the result of parsing bytes.
|
|
319
|
-
let entries
|
|
320
|
-
try {
|
|
321
|
-
entries = new URLSearchParams(await this.text())
|
|
322
|
-
} catch (err) {
|
|
323
|
-
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
|
|
324
|
-
// 2. If entries is failure, then throw a TypeError.
|
|
325
|
-
throw Object.assign(new TypeError(), { cause: err })
|
|
319
|
+
async json () {
|
|
320
|
+
if (!(this instanceof instance)) {
|
|
321
|
+
throw new TypeError('Illegal invocation')
|
|
326
322
|
}
|
|
327
323
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
324
|
+
return JSON.parse(await this.text())
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
async formData () {
|
|
328
|
+
if (!(this instanceof instance)) {
|
|
329
|
+
throw new TypeError('Illegal invocation')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const contentType = this.headers.get('Content-Type')
|
|
333
|
+
|
|
334
|
+
// If mimeType’s essence is "multipart/form-data", then:
|
|
335
|
+
if (/multipart\/form-data/.test(contentType)) {
|
|
336
|
+
throw new NotSupportedError('multipart/form-data not supported')
|
|
337
|
+
} else if (/application\/x-www-form-urlencoded/.test(contentType)) {
|
|
338
|
+
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
|
|
339
|
+
|
|
340
|
+
// 1. Let entries be the result of parsing bytes.
|
|
341
|
+
let entries
|
|
342
|
+
try {
|
|
343
|
+
entries = new URLSearchParams(await this.text())
|
|
344
|
+
} catch (err) {
|
|
345
|
+
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
|
|
346
|
+
// 2. If entries is failure, then throw a TypeError.
|
|
347
|
+
throw Object.assign(new TypeError(), { cause: err })
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 3. Return a new FormData object whose entries are entries.
|
|
351
|
+
const formData = new FormData()
|
|
352
|
+
for (const [name, value] of entries) {
|
|
353
|
+
formData.append(name, value)
|
|
354
|
+
}
|
|
355
|
+
return formData
|
|
356
|
+
} else {
|
|
357
|
+
// Otherwise, throw a TypeError.
|
|
358
|
+
webidl.errors.exception({
|
|
359
|
+
header: `${instance.name}.formData`,
|
|
360
|
+
value: 'Could not parse content as FormData.'
|
|
361
|
+
})
|
|
332
362
|
}
|
|
333
|
-
return formData
|
|
334
|
-
} else {
|
|
335
|
-
// Otherwise, throw a TypeError.
|
|
336
|
-
throw new TypeError()
|
|
337
363
|
}
|
|
338
364
|
}
|
|
365
|
+
|
|
366
|
+
return methods
|
|
339
367
|
}
|
|
340
368
|
|
|
341
369
|
const properties = {
|
|
342
370
|
body: {
|
|
343
371
|
enumerable: true,
|
|
344
372
|
get () {
|
|
373
|
+
if (!this || !this[kState]) {
|
|
374
|
+
throw new TypeError('Illegal invocation')
|
|
375
|
+
}
|
|
376
|
+
|
|
345
377
|
return this[kState].body ? this[kState].body.stream : null
|
|
346
378
|
}
|
|
347
379
|
},
|
|
348
380
|
bodyUsed: {
|
|
349
381
|
enumerable: true,
|
|
350
382
|
get () {
|
|
383
|
+
if (!this || !this[kState]) {
|
|
384
|
+
throw new TypeError('Illegal invocation')
|
|
385
|
+
}
|
|
386
|
+
|
|
351
387
|
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
|
|
352
388
|
}
|
|
353
389
|
}
|
|
354
390
|
}
|
|
355
391
|
|
|
356
392
|
function mixinBody (prototype) {
|
|
357
|
-
Object.assign(prototype,
|
|
358
|
-
Object.defineProperties(prototype, properties)
|
|
393
|
+
Object.assign(prototype.prototype, bodyMixinMethods(prototype))
|
|
394
|
+
Object.defineProperties(prototype.prototype, properties)
|
|
359
395
|
}
|
|
360
396
|
|
|
361
397
|
module.exports = {
|
package/lib/fetch/constants.js
CHANGED
|
@@ -60,7 +60,19 @@ const subresource = [
|
|
|
60
60
|
''
|
|
61
61
|
]
|
|
62
62
|
|
|
63
|
+
/** @type {globalThis['DOMException']} */
|
|
64
|
+
const DOMException = globalThis.DOMException ?? (() => {
|
|
65
|
+
// DOMException was only made a global in Node v17.0.0,
|
|
66
|
+
// but fetch supports >= v16.5.
|
|
67
|
+
try {
|
|
68
|
+
atob('~')
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return Object.getPrototypeOf(err).constructor
|
|
71
|
+
}
|
|
72
|
+
})()
|
|
73
|
+
|
|
63
74
|
module.exports = {
|
|
75
|
+
DOMException,
|
|
64
76
|
subresource,
|
|
65
77
|
forbiddenMethods,
|
|
66
78
|
requestBodyHeader,
|
package/lib/fetch/file.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Blob } = require('buffer')
|
|
4
|
+
const { types } = require('util')
|
|
4
5
|
const { kState } = require('./symbols')
|
|
6
|
+
const { isBlobLike } = require('./util')
|
|
7
|
+
const { webidl } = require('./webidl')
|
|
5
8
|
|
|
6
9
|
class File extends Blob {
|
|
7
10
|
constructor (fileBits, fileName, options = {}) {
|
|
8
|
-
// TODO: argument idl type check
|
|
9
|
-
|
|
10
11
|
// The File constructor is invoked with two or three parameters, depending
|
|
11
12
|
// on whether the optional dictionary parameter is used. When the File()
|
|
12
13
|
// constructor is invoked, user agents must run the following steps:
|
|
14
|
+
if (arguments.length < 2) {
|
|
15
|
+
throw new TypeError('2 arguments required')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
|
|
19
|
+
fileName = webidl.converters.USVString(fileName)
|
|
20
|
+
options = webidl.converters.FilePropertyBag(options)
|
|
13
21
|
|
|
14
22
|
// 1. Let bytes be the result of processing blob parts given fileBits and
|
|
15
23
|
// options.
|
|
16
|
-
//
|
|
24
|
+
// Note: Blob handles this for us
|
|
17
25
|
|
|
18
26
|
// 2. Let n be the fileName argument to the constructor.
|
|
19
27
|
const n = fileName
|
|
@@ -25,17 +33,14 @@ class File extends Blob {
|
|
|
25
33
|
// be set to the type dictionary member. If t contains any characters
|
|
26
34
|
// outside the range U+0020 to U+007E, then set t to the empty string
|
|
27
35
|
// and return from these substeps.
|
|
28
|
-
// TODO
|
|
29
|
-
const t = options.type
|
|
30
|
-
|
|
31
36
|
// 2. Convert every character in t to ASCII lowercase.
|
|
32
|
-
//
|
|
37
|
+
// Note: Blob handles both of these steps for us
|
|
33
38
|
|
|
34
39
|
// 3. If the lastModified member is provided, let d be set to the
|
|
35
40
|
// lastModified dictionary member. If it is not provided, set d to the
|
|
36
41
|
// current date and time represented as the number of milliseconds since
|
|
37
42
|
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
|
|
38
|
-
const d = options.lastModified
|
|
43
|
+
const d = options.lastModified
|
|
39
44
|
|
|
40
45
|
// 4. Return a new File object F such that:
|
|
41
46
|
// F refers to the bytes byte sequence.
|
|
@@ -43,9 +48,8 @@ class File extends Blob {
|
|
|
43
48
|
// F.name is set to n.
|
|
44
49
|
// F.type is set to t.
|
|
45
50
|
// F.lastModified is set to d.
|
|
46
|
-
// TODO
|
|
47
51
|
|
|
48
|
-
super(fileBits, { type:
|
|
52
|
+
super(processBlobParts(fileBits, options), { type: options.type })
|
|
49
53
|
this[kState] = {
|
|
50
54
|
name: n,
|
|
51
55
|
lastModified: d
|
|
@@ -190,4 +194,120 @@ class FileLike {
|
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
196
|
|
|
193
|
-
|
|
197
|
+
webidl.converters.Blob = webidl.interfaceConverter(Blob)
|
|
198
|
+
|
|
199
|
+
webidl.converters.BlobPart = function (V, opts) {
|
|
200
|
+
if (webidl.util.Type(V) === 'Object') {
|
|
201
|
+
if (isBlobLike(V)) {
|
|
202
|
+
return webidl.converters.Blob(V, { strict: false })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return webidl.converters.BufferSource(V, opts)
|
|
206
|
+
} else {
|
|
207
|
+
return webidl.converters.USVString(V, opts)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
|
|
212
|
+
webidl.converters.BlobPart
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
|
|
216
|
+
webidl.converters.FilePropertyBag = webidl.dictionaryConverter([
|
|
217
|
+
{
|
|
218
|
+
key: 'lastModified',
|
|
219
|
+
converter: webidl.converters['long long'],
|
|
220
|
+
get defaultValue () {
|
|
221
|
+
return Date.now()
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
key: 'type',
|
|
226
|
+
converter: webidl.converters.DOMString,
|
|
227
|
+
defaultValue: ''
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: 'endings',
|
|
231
|
+
converter: (value) => {
|
|
232
|
+
value = webidl.converters.DOMString(value)
|
|
233
|
+
value = value.toLowerCase()
|
|
234
|
+
|
|
235
|
+
if (value !== 'native') {
|
|
236
|
+
value = 'transparent'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return value
|
|
240
|
+
},
|
|
241
|
+
defaultValue: 'transparent'
|
|
242
|
+
}
|
|
243
|
+
])
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @see https://www.w3.org/TR/FileAPI/#process-blob-parts
|
|
247
|
+
* @param {(NodeJS.TypedArray|Blob|string)[]} parts
|
|
248
|
+
* @param {{ type: string, endings: string }} options
|
|
249
|
+
*/
|
|
250
|
+
function processBlobParts (parts, options) {
|
|
251
|
+
// 1. Let bytes be an empty sequence of bytes.
|
|
252
|
+
/** @type {NodeJS.TypedArray[]} */
|
|
253
|
+
const bytes = []
|
|
254
|
+
|
|
255
|
+
// 2. For each element in parts:
|
|
256
|
+
for (const element of parts) {
|
|
257
|
+
// 1. If element is a USVString, run the following substeps:
|
|
258
|
+
if (typeof element === 'string') {
|
|
259
|
+
// 1. Let s be element.
|
|
260
|
+
let s = element
|
|
261
|
+
|
|
262
|
+
// 2. If the endings member of options is "native", set s
|
|
263
|
+
// to the result of converting line endings to native
|
|
264
|
+
// of element.
|
|
265
|
+
if (options.endings === 'native') {
|
|
266
|
+
s = convertLineEndingsNative(s)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 3. Append the result of UTF-8 encoding s to bytes.
|
|
270
|
+
bytes.push(new TextEncoder().encode(s))
|
|
271
|
+
} else if (
|
|
272
|
+
types.isAnyArrayBuffer(element) ||
|
|
273
|
+
types.isTypedArray(element)
|
|
274
|
+
) {
|
|
275
|
+
// 2. If element is a BufferSource, get a copy of the
|
|
276
|
+
// bytes held by the buffer source, and append those
|
|
277
|
+
// bytes to bytes.
|
|
278
|
+
if (!element.buffer) { // ArrayBuffer
|
|
279
|
+
bytes.push(new Uint8Array(element))
|
|
280
|
+
} else {
|
|
281
|
+
bytes.push(element.buffer)
|
|
282
|
+
}
|
|
283
|
+
} else if (isBlobLike(element)) {
|
|
284
|
+
// 3. If element is a Blob, append the bytes it represents
|
|
285
|
+
// to bytes.
|
|
286
|
+
bytes.push(element)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 3. Return bytes.
|
|
291
|
+
return bytes
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native
|
|
296
|
+
* @param {string} s
|
|
297
|
+
*/
|
|
298
|
+
function convertLineEndingsNative (s) {
|
|
299
|
+
// 1. Let native line ending be be the code point U+000A LF.
|
|
300
|
+
let nativeLineEnding = '\n'
|
|
301
|
+
|
|
302
|
+
// 2. If the underlying platform’s conventions are to
|
|
303
|
+
// represent newlines as a carriage return and line feed
|
|
304
|
+
// sequence, set native line ending to the code point
|
|
305
|
+
// U+000D CR followed by the code point U+000A LF.
|
|
306
|
+
if (process.platform === 'win32') {
|
|
307
|
+
nativeLineEnding = '\r\n'
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return s.replace(/\r?\n/g, nativeLineEnding)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = { File, FileLike }
|