undici 7.19.2 → 7.20.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/docs/docs/api/Dispatcher.md +2 -1
- package/docs/docs/api/MockPool.md +2 -1
- package/index-fetch.js +25 -3
- package/index.js +25 -3
- package/lib/api/api-request.js +1 -0
- package/lib/dispatcher/client-h1.js +7 -2
- package/lib/mock/mock-utils.js +35 -2
- package/lib/web/fetch/body.js +34 -48
- package/lib/web/fetch/index.js +2 -2
- package/lib/web/webidl/index.js +52 -0
- package/package.json +1 -1
- package/types/dispatcher.d.ts +1 -0
- package/types/webidl.d.ts +6 -0
|
@@ -476,6 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
|
|
|
476
476
|
#### Parameter: `ResponseData`
|
|
477
477
|
|
|
478
478
|
* **statusCode** `number`
|
|
479
|
+
* **statusText** `string` - The status message from the response (e.g., "OK", "Not Found").
|
|
479
480
|
* **headers** `Record<string, string | string[]>` - Note that all header keys are lower-cased, e.g. `content-type`.
|
|
480
481
|
* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
|
|
481
482
|
* **trailers** `Record<string, string>` - This object starts out
|
|
@@ -517,7 +518,7 @@ await once(server, 'listening')
|
|
|
517
518
|
const client = new Client(`http://localhost:${server.address().port}`)
|
|
518
519
|
|
|
519
520
|
try {
|
|
520
|
-
const { body, headers, statusCode, trailers } = await client.request({
|
|
521
|
+
const { body, headers, statusCode, statusText, trailers } = await client.request({
|
|
521
522
|
path: '/',
|
|
522
523
|
method: 'GET'
|
|
523
524
|
})
|
package/index-fetch.js
CHANGED
|
@@ -4,11 +4,33 @@ const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
|
|
|
4
4
|
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent')
|
|
5
5
|
const fetchImpl = require('./lib/web/fetch').fetch
|
|
6
6
|
|
|
7
|
+
function appendFetchStackTrace (err, filename) {
|
|
8
|
+
if (!err || typeof err !== 'object') {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const stack = typeof err.stack === 'string' ? err.stack : ''
|
|
13
|
+
const normalizedFilename = filename.replace(/\\/g, '/')
|
|
14
|
+
|
|
15
|
+
if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const capture = {}
|
|
20
|
+
Error.captureStackTrace(capture, appendFetchStackTrace)
|
|
21
|
+
|
|
22
|
+
if (!capture.stack) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const captureLines = capture.stack.split('\n').slice(1).join('\n')
|
|
27
|
+
|
|
28
|
+
err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
module.exports.fetch = function fetch (init, options = undefined) {
|
|
8
32
|
return fetchImpl(init, options).catch(err => {
|
|
9
|
-
|
|
10
|
-
Error.captureStackTrace(err)
|
|
11
|
-
}
|
|
33
|
+
appendFetchStackTrace(err, __filename)
|
|
12
34
|
throw err
|
|
13
35
|
})
|
|
14
36
|
}
|
package/index.js
CHANGED
|
@@ -121,11 +121,33 @@ module.exports.getGlobalDispatcher = getGlobalDispatcher
|
|
|
121
121
|
|
|
122
122
|
const fetchImpl = require('./lib/web/fetch').fetch
|
|
123
123
|
|
|
124
|
+
function appendFetchStackTrace (err, filename) {
|
|
125
|
+
if (!err || typeof err !== 'object') {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const stack = typeof err.stack === 'string' ? err.stack : ''
|
|
130
|
+
const normalizedFilename = filename.replace(/\\/g, '/')
|
|
131
|
+
|
|
132
|
+
if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const capture = {}
|
|
137
|
+
Error.captureStackTrace(capture, appendFetchStackTrace)
|
|
138
|
+
|
|
139
|
+
if (!capture.stack) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const captureLines = capture.stack.split('\n').slice(1).join('\n')
|
|
144
|
+
|
|
145
|
+
err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
|
|
146
|
+
}
|
|
147
|
+
|
|
124
148
|
module.exports.fetch = function fetch (init, options = undefined) {
|
|
125
149
|
return fetchImpl(init, options).catch(err => {
|
|
126
|
-
|
|
127
|
-
Error.captureStackTrace(err)
|
|
128
|
-
}
|
|
150
|
+
appendFetchStackTrace(err, __filename)
|
|
129
151
|
throw err
|
|
130
152
|
})
|
|
131
153
|
}
|
package/lib/api/api-request.js
CHANGED
|
@@ -735,8 +735,13 @@ class Parser {
|
|
|
735
735
|
}
|
|
736
736
|
}
|
|
737
737
|
|
|
738
|
-
function onParserTimeout (
|
|
739
|
-
const
|
|
738
|
+
function onParserTimeout (parserWeakRef) {
|
|
739
|
+
const parser = parserWeakRef.deref()
|
|
740
|
+
if (!parser) {
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const { socket, timeoutType, client, paused } = parser
|
|
740
745
|
|
|
741
746
|
if (timeoutType === TIMEOUT_HEADERS) {
|
|
742
747
|
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -312,9 +312,33 @@ function mockDispatch (opts, handler) {
|
|
|
312
312
|
return true
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// Track whether the request has been aborted
|
|
316
|
+
let aborted = false
|
|
317
|
+
let timer = null
|
|
318
|
+
|
|
319
|
+
function abort (err) {
|
|
320
|
+
if (aborted) {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
aborted = true
|
|
324
|
+
|
|
325
|
+
// Clear the pending delayed response if any
|
|
326
|
+
if (timer !== null) {
|
|
327
|
+
clearTimeout(timer)
|
|
328
|
+
timer = null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Notify the handler of the abort
|
|
332
|
+
handler.onError(err)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Call onConnect to allow the handler to register the abort callback
|
|
336
|
+
handler.onConnect?.(abort, null)
|
|
337
|
+
|
|
315
338
|
// Handle the request with a delay if necessary
|
|
316
339
|
if (typeof delay === 'number' && delay > 0) {
|
|
317
|
-
setTimeout(() => {
|
|
340
|
+
timer = setTimeout(() => {
|
|
341
|
+
timer = null
|
|
318
342
|
handleReply(this[kDispatches])
|
|
319
343
|
}, delay)
|
|
320
344
|
} else {
|
|
@@ -322,6 +346,11 @@ function mockDispatch (opts, handler) {
|
|
|
322
346
|
}
|
|
323
347
|
|
|
324
348
|
function handleReply (mockDispatches, _data = data) {
|
|
349
|
+
// Don't send response if the request was aborted
|
|
350
|
+
if (aborted) {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
325
354
|
// fetch's HeadersList is a 1D string array
|
|
326
355
|
const optsHeaders = Array.isArray(opts.headers)
|
|
327
356
|
? buildHeadersFromArray(opts.headers)
|
|
@@ -340,11 +369,15 @@ function mockDispatch (opts, handler) {
|
|
|
340
369
|
return body.then((newData) => handleReply(mockDispatches, newData))
|
|
341
370
|
}
|
|
342
371
|
|
|
372
|
+
// Check again if aborted after async body resolution
|
|
373
|
+
if (aborted) {
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
343
377
|
const responseData = getResponseData(body)
|
|
344
378
|
const responseHeaders = generateKeyValues(headers)
|
|
345
379
|
const responseTrailers = generateKeyValues(trailers)
|
|
346
380
|
|
|
347
|
-
handler.onConnect?.(err => handler.onError(err), null)
|
|
348
381
|
handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
|
|
349
382
|
handler.onData?.(Buffer.from(responseData))
|
|
350
383
|
handler.onComplete?.(responseTrailers)
|
package/lib/web/fetch/body.js
CHANGED
|
@@ -11,7 +11,7 @@ const { FormData, setFormDataState } = require('./formdata')
|
|
|
11
11
|
const { webidl } = require('../webidl')
|
|
12
12
|
const assert = require('node:assert')
|
|
13
13
|
const { isErrored, isDisturbed } = require('node:stream')
|
|
14
|
-
const {
|
|
14
|
+
const { isUint8Array } = require('node:util/types')
|
|
15
15
|
const { serializeAMimeType } = require('./data-url')
|
|
16
16
|
const { multipartFormDataParser } = require('./formdata-parser')
|
|
17
17
|
const { createDeferredPromise } = require('../../util/promise')
|
|
@@ -45,6 +45,7 @@ const streamRegistry = new FinalizationRegistry((weakRef) => {
|
|
|
45
45
|
function extractBody (object, keepalive = false) {
|
|
46
46
|
// 1. Let stream be null.
|
|
47
47
|
let stream = null
|
|
48
|
+
let controller = null
|
|
48
49
|
|
|
49
50
|
// 2. If object is a ReadableStream object, then set stream to object.
|
|
50
51
|
if (webidl.is.ReadableStream(object)) {
|
|
@@ -57,16 +58,11 @@ function extractBody (object, keepalive = false) {
|
|
|
57
58
|
// 4. Otherwise, set stream to a new ReadableStream object, and set
|
|
58
59
|
// up stream with byte reading support.
|
|
59
60
|
stream = new ReadableStream({
|
|
60
|
-
pull (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (buffer.byteLength) {
|
|
64
|
-
controller.enqueue(buffer)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
queueMicrotask(() => readableStreamClose(controller))
|
|
61
|
+
pull () {},
|
|
62
|
+
start (c) {
|
|
63
|
+
controller = c
|
|
68
64
|
},
|
|
69
|
-
|
|
65
|
+
cancel () {},
|
|
70
66
|
type: 'bytes'
|
|
71
67
|
})
|
|
72
68
|
}
|
|
@@ -108,9 +104,8 @@ function extractBody (object, keepalive = false) {
|
|
|
108
104
|
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
|
|
109
105
|
type = 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
110
106
|
} else if (webidl.is.BufferSource(object)) {
|
|
111
|
-
source
|
|
112
|
-
|
|
113
|
-
: new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
|
|
107
|
+
// Set source to a copy of the bytes held by object.
|
|
108
|
+
source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
|
|
114
109
|
} else if (webidl.is.FormData(object)) {
|
|
115
110
|
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
|
|
116
111
|
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
|
|
@@ -213,45 +208,36 @@ function extractBody (object, keepalive = false) {
|
|
|
213
208
|
|
|
214
209
|
// 11. If source is a byte sequence, then set action to a
|
|
215
210
|
// step that returns source and length to source’s length.
|
|
216
|
-
if (typeof source === 'string' ||
|
|
217
|
-
|
|
211
|
+
if (typeof source === 'string' || isUint8Array(source)) {
|
|
212
|
+
action = () => {
|
|
213
|
+
length = typeof source === 'string' ? Buffer.byteLength(source) : source.length
|
|
214
|
+
return source
|
|
215
|
+
}
|
|
218
216
|
}
|
|
219
217
|
|
|
220
|
-
// 12. If action is non-null, then run these steps in
|
|
218
|
+
// 12. If action is non-null, then run these steps in parallel:
|
|
221
219
|
if (action != null) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
controller.byobRequest?.respond(0)
|
|
235
|
-
})
|
|
236
|
-
} else {
|
|
237
|
-
// Whenever one or more bytes are available and stream is not errored,
|
|
238
|
-
// enqueue a Uint8Array wrapping an ArrayBuffer containing the available
|
|
239
|
-
// bytes into stream.
|
|
240
|
-
if (!isErrored(stream)) {
|
|
241
|
-
const buffer = new Uint8Array(value)
|
|
242
|
-
if (buffer.byteLength) {
|
|
243
|
-
controller.enqueue(buffer)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
220
|
+
;(async () => {
|
|
221
|
+
// 1. Run action.
|
|
222
|
+
const result = action()
|
|
223
|
+
|
|
224
|
+
// 2. Whenever one or more bytes are available and stream is not errored,
|
|
225
|
+
// enqueue the result of creating a Uint8Array from the available bytes into stream.
|
|
226
|
+
const iterator = result?.[Symbol.asyncIterator]?.()
|
|
227
|
+
if (iterator) {
|
|
228
|
+
for await (const bytes of iterator) {
|
|
229
|
+
if (isErrored(stream)) break
|
|
230
|
+
if (bytes.length) {
|
|
231
|
+
controller.enqueue(new Uint8Array(bytes))
|
|
246
232
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})
|
|
233
|
+
}
|
|
234
|
+
} else if (result?.length && !isErrored(stream)) {
|
|
235
|
+
controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 3. When running action is done, close stream.
|
|
239
|
+
queueMicrotask(() => readableStreamClose(controller))
|
|
240
|
+
})()
|
|
255
241
|
}
|
|
256
242
|
|
|
257
243
|
// 13. Let body be a body whose stream is stream, source is source,
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -1321,8 +1321,8 @@ function httpRedirectFetch (fetchParams, response) {
|
|
|
1321
1321
|
request.headersList.delete('host', true)
|
|
1322
1322
|
}
|
|
1323
1323
|
|
|
1324
|
-
// 14. If request
|
|
1325
|
-
// value of safely extracting request
|
|
1324
|
+
// 14. If request's body is non-null, then set request's body to the first return
|
|
1325
|
+
// value of safely extracting request's body's source.
|
|
1326
1326
|
if (request.body != null) {
|
|
1327
1327
|
assert(request.body.source != null)
|
|
1328
1328
|
request.body = safelyExtractBody(request.body.source)[0]
|
package/lib/web/webidl/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
3
4
|
const { types, inspect } = require('node:util')
|
|
4
5
|
const { runtimeFeatures } = require('../../util/runtime-features')
|
|
5
6
|
|
|
@@ -542,6 +543,57 @@ webidl.is.BufferSource = function (V) {
|
|
|
542
543
|
)
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
// https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
|
|
547
|
+
webidl.util.getCopyOfBytesHeldByBufferSource = function (bufferSource) {
|
|
548
|
+
// 1. Let jsBufferSource be the result of converting bufferSource to a JavaScript value.
|
|
549
|
+
const jsBufferSource = bufferSource
|
|
550
|
+
|
|
551
|
+
// 2. Let jsArrayBuffer be jsBufferSource.
|
|
552
|
+
let jsArrayBuffer = jsBufferSource
|
|
553
|
+
|
|
554
|
+
// 3. Let offset be 0.
|
|
555
|
+
let offset = 0
|
|
556
|
+
|
|
557
|
+
// 4. Let length be 0.
|
|
558
|
+
let length = 0
|
|
559
|
+
|
|
560
|
+
// 5. If jsBufferSource has a [[ViewedArrayBuffer]] internal slot, then:
|
|
561
|
+
if (types.isTypedArray(jsBufferSource) || types.isDataView(jsBufferSource)) {
|
|
562
|
+
// 5.1. Set jsArrayBuffer to jsBufferSource.[[ViewedArrayBuffer]].
|
|
563
|
+
jsArrayBuffer = jsBufferSource.buffer
|
|
564
|
+
|
|
565
|
+
// 5.2. Set offset to jsBufferSource.[[ByteOffset]].
|
|
566
|
+
offset = jsBufferSource.byteOffset
|
|
567
|
+
|
|
568
|
+
// 5.3. Set length to jsBufferSource.[[ByteLength]].
|
|
569
|
+
length = jsBufferSource.byteLength
|
|
570
|
+
} else {
|
|
571
|
+
// 6. Otherwise:
|
|
572
|
+
|
|
573
|
+
// 6.1. Assert: jsBufferSource is an ArrayBuffer or SharedArrayBuffer object.
|
|
574
|
+
assert(types.isAnyArrayBuffer(jsBufferSource))
|
|
575
|
+
|
|
576
|
+
// 6.2. Set length to jsBufferSource.[[ArrayBufferByteLength]].
|
|
577
|
+
length = jsBufferSource.byteLength
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 7. If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence.
|
|
581
|
+
if (jsArrayBuffer.detached) {
|
|
582
|
+
return new Uint8Array(0)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// 8. Let bytes be a new byte sequence of length equal to length.
|
|
586
|
+
const bytes = new Uint8Array(length)
|
|
587
|
+
|
|
588
|
+
// 9. For i in the range offset to offset + length − 1, inclusive,
|
|
589
|
+
// set bytes[i − offset] to GetValueFromBuffer(jsArrayBuffer, i, Uint8, true, Unordered).
|
|
590
|
+
const view = new Uint8Array(jsArrayBuffer, offset, length)
|
|
591
|
+
bytes.set(view)
|
|
592
|
+
|
|
593
|
+
// 10. Return bytes.
|
|
594
|
+
return bytes
|
|
595
|
+
}
|
|
596
|
+
|
|
545
597
|
// https://webidl.spec.whatwg.org/#es-DOMString
|
|
546
598
|
webidl.converters.DOMString = function (V, prefix, argument, flags) {
|
|
547
599
|
// 1. If V is null and the conversion is to an IDL type
|
package/package.json
CHANGED
package/types/dispatcher.d.ts
CHANGED
package/types/webidl.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// These types are not exported, and are only used internally
|
|
2
|
+
import { BufferSource } from 'node:stream/web'
|
|
2
3
|
import * as undici from './index'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -93,6 +94,11 @@ interface WebidlUtil {
|
|
|
93
94
|
IsResizableArrayBuffer (V: ArrayBufferLike): boolean
|
|
94
95
|
|
|
95
96
|
HasFlag (flag: number, attributes: number): boolean
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @see https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
|
|
100
|
+
*/
|
|
101
|
+
getCopyOfBytesHeldByBufferSource (bufferSource: BufferSource): Uint8Array
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
interface WebidlConverters {
|