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.
@@ -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
  })
@@ -323,7 +323,8 @@ try {
323
323
  method: 'GET'
324
324
  })
325
325
  } catch (error) {
326
- console.error(error) // Error: kaboom
326
+ console.error(error) // TypeError: fetch failed
327
+ console.error(error.cause) // Error: kaboom
327
328
  }
328
329
  ```
329
330
 
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
- if (err && typeof err === 'object') {
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
- if (err && typeof err === 'object') {
127
- Error.captureStackTrace(err)
128
- }
150
+ appendFetchStackTrace(err, __filename)
129
151
  throw err
130
152
  })
131
153
  }
@@ -121,6 +121,7 @@ class RequestHandler extends AsyncResource {
121
121
  try {
122
122
  this.runInAsyncScope(callback, null, null, {
123
123
  statusCode,
124
+ statusText: statusMessage,
124
125
  headers,
125
126
  trailers: this.trailers,
126
127
  opaque,
@@ -735,8 +735,13 @@ class Parser {
735
735
  }
736
736
  }
737
737
 
738
- function onParserTimeout (parser) {
739
- const { socket, timeoutType, client, paused } = parser.deref()
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) {
@@ -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)
@@ -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 { isArrayBuffer } = require('node:util/types')
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 (controller) {
61
- const buffer = typeof source === 'string' ? textEncoder.encode(source) : source
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
- start () {},
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 = isArrayBuffer(object)
112
- ? new Uint8Array(object.slice())
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' || util.isBuffer(source)) {
217
- length = Buffer.byteLength(source)
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 in parallel:
218
+ // 12. If action is non-null, then run these steps in parallel:
221
219
  if (action != null) {
222
- // Run action.
223
- let iterator
224
- stream = new ReadableStream({
225
- start () {
226
- iterator = action(object)[Symbol.asyncIterator]()
227
- },
228
- pull (controller) {
229
- return iterator.next().then(({ value, done }) => {
230
- if (done) {
231
- // When running action is done, close stream.
232
- queueMicrotask(() => {
233
- controller.close()
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
- return controller.desiredSize > 0
248
- })
249
- },
250
- cancel (reason) {
251
- return iterator.return()
252
- },
253
- type: 'bytes'
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,
@@ -1321,8 +1321,8 @@ function httpRedirectFetch (fetchParams, response) {
1321
1321
  request.headersList.delete('host', true)
1322
1322
  }
1323
1323
 
1324
- // 14. If requests body is non-null, then set requests body to the first return
1325
- // value of safely extracting requests bodys source.
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]
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.19.2",
3
+ "version": "7.20.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": {
@@ -181,6 +181,7 @@ declare namespace Dispatcher {
181
181
  }
182
182
  export interface ResponseData<TOpaque = null> {
183
183
  statusCode: number;
184
+ statusText: string;
184
185
  headers: IncomingHttpHeaders;
185
186
  body: BodyReadable & BodyMixin;
186
187
  trailers: Record<string, string>;
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 {