undici 5.28.0 → 5.28.2

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.
@@ -177,3 +177,4 @@ function request (opts, callback) {
177
177
  }
178
178
 
179
179
  module.exports = request
180
+ module.exports.RequestHandler = RequestHandler
@@ -180,7 +180,7 @@ module.exports = class BodyReadable extends Readable {
180
180
  this
181
181
  .on('close', function () {
182
182
  signalListenerCleanup()
183
- if (signal?.aborted) {
183
+ if (signal && signal.aborted) {
184
184
  reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }))
185
185
  } else {
186
186
  resolve(null)
@@ -1,5 +1,5 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
- kConstruct: Symbol('constructable')
4
+ kConstruct: require('../core/symbols').kConstruct
5
5
  }
package/lib/client.js CHANGED
@@ -917,11 +917,9 @@ class Parser {
917
917
  socket[kReset] = true
918
918
  }
919
919
 
920
- let pause
921
- try {
922
- pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
923
- } catch (err) {
924
- util.destroy(socket, err)
920
+ const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
921
+
922
+ if (request.aborted) {
925
923
  return -1
926
924
  }
927
925
 
@@ -968,13 +966,8 @@ class Parser {
968
966
 
969
967
  this.bytesRead += buf.length
970
968
 
971
- try {
972
- if (request.onData(buf) === false) {
973
- return constants.ERROR.PAUSED
974
- }
975
- } catch (err) {
976
- util.destroy(socket, err)
977
- return -1
969
+ if (request.onData(buf) === false) {
970
+ return constants.ERROR.PAUSED
978
971
  }
979
972
  }
980
973
 
@@ -1015,11 +1008,7 @@ class Parser {
1015
1008
  return -1
1016
1009
  }
1017
1010
 
1018
- try {
1019
- request.onComplete(headers)
1020
- } catch (err) {
1021
- errorRequest(client, request, err)
1022
- }
1011
+ request.onComplete(headers)
1023
1012
 
1024
1013
  client[kQueue][client[kRunningIdx]++] = null
1025
1014
 
@@ -1805,13 +1794,17 @@ function writeH2 (client, session, request) {
1805
1794
  })
1806
1795
 
1807
1796
  stream.on('data', (chunk) => {
1808
- if (request.onData(chunk) === false) stream.pause()
1797
+ if (request.onData(chunk) === false) {
1798
+ stream.pause()
1799
+ }
1809
1800
  })
1810
1801
 
1811
1802
  stream.once('close', () => {
1812
1803
  h2State.openStreams -= 1
1813
1804
  // TODO(HTTP/2): unref only if current streams count is 0
1814
- if (h2State.openStreams === 0) session.unref()
1805
+ if (h2State.openStreams === 0) {
1806
+ session.unref()
1807
+ }
1815
1808
  })
1816
1809
 
1817
1810
  stream.once('error', function (err) {
@@ -229,7 +229,11 @@ class Request {
229
229
 
230
230
  onBodySent (chunk) {
231
231
  if (this[kHandler].onBodySent) {
232
- return this[kHandler].onBodySent(chunk)
232
+ try {
233
+ return this[kHandler].onBodySent(chunk)
234
+ } catch (err) {
235
+ this.abort(err)
236
+ }
233
237
  }
234
238
  }
235
239
 
@@ -239,7 +243,11 @@ class Request {
239
243
  }
240
244
 
241
245
  if (this[kHandler].onRequestSent) {
242
- return this[kHandler].onRequestSent()
246
+ try {
247
+ return this[kHandler].onRequestSent()
248
+ } catch (err) {
249
+ this.abort(err)
250
+ }
243
251
  }
244
252
  }
245
253
 
@@ -263,14 +271,23 @@ class Request {
263
271
  channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
264
272
  }
265
273
 
266
- return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
274
+ try {
275
+ return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
276
+ } catch (err) {
277
+ this.abort(err)
278
+ }
267
279
  }
268
280
 
269
281
  onData (chunk) {
270
282
  assert(!this.aborted)
271
283
  assert(!this.completed)
272
284
 
273
- return this[kHandler].onData(chunk)
285
+ try {
286
+ return this[kHandler].onData(chunk)
287
+ } catch (err) {
288
+ this.abort(err)
289
+ return false
290
+ }
274
291
  }
275
292
 
276
293
  onUpgrade (statusCode, headers, socket) {
@@ -289,7 +306,13 @@ class Request {
289
306
  if (channels.trailers.hasSubscribers) {
290
307
  channels.trailers.publish({ request: this, trailers })
291
308
  }
292
- return this[kHandler].onComplete(trailers)
309
+
310
+ try {
311
+ return this[kHandler].onComplete(trailers)
312
+ } catch (err) {
313
+ // TODO (fix): This might be a bad idea?
314
+ this.onError(err)
315
+ }
293
316
  }
294
317
 
295
318
  onError (error) {
@@ -303,6 +326,7 @@ class Request {
303
326
  return
304
327
  }
305
328
  this.aborted = true
329
+
306
330
  return this[kHandler].onError(error)
307
331
  }
308
332
 
@@ -58,5 +58,6 @@ module.exports = {
58
58
  kHTTP1BuildRequest: Symbol('http1 build request'),
59
59
  kHTTP2CopyHeaders: Symbol('http2 copy headers'),
60
60
  kHTTPConnVersion: Symbol('http connection version'),
61
- kRetryHandlerDefaultRetry: Symbol('retry agent default retry')
61
+ kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
62
+ kConstruct: Symbol('constructable')
62
63
  }
@@ -119,17 +119,14 @@ function dataURLProcessor (dataURL) {
119
119
  * @param {boolean} excludeFragment
120
120
  */
121
121
  function URLSerializer (url, excludeFragment = false) {
122
- const href = url.href
123
-
124
122
  if (!excludeFragment) {
125
- return href
123
+ return url.href
126
124
  }
127
125
 
128
- const hash = href.lastIndexOf('#')
129
- if (hash === -1) {
130
- return href
131
- }
132
- return href.slice(0, hash)
126
+ const href = url.href
127
+ const hashLength = url.hash.length
128
+
129
+ return hashLength === 0 ? href : href.substring(0, href.length - hashLength)
133
130
  }
134
131
 
135
132
  // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
@@ -2,7 +2,7 @@
2
2
 
3
3
  'use strict'
4
4
 
5
- const { kHeadersList } = require('../core/symbols')
5
+ const { kHeadersList, kConstruct } = require('../core/symbols')
6
6
  const { kGuard } = require('./symbols')
7
7
  const { kEnumerableProperty } = require('../core/util')
8
8
  const {
@@ -240,6 +240,9 @@ class HeadersList {
240
240
  // https://fetch.spec.whatwg.org/#headers-class
241
241
  class Headers {
242
242
  constructor (init = undefined) {
243
+ if (init === kConstruct) {
244
+ return
245
+ }
243
246
  this[kHeadersList] = new HeadersList()
244
247
 
245
248
  // The new Headers(init) constructor steps are:
@@ -286,7 +286,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
286
286
  }
287
287
 
288
288
  // 8. If response’s timing allow passed flag is not set, then:
289
- if (!timingInfo.timingAllowPassed) {
289
+ if (!response.timingAllowPassed) {
290
290
  // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo.
291
291
  timingInfo = createOpaqueTimingInfo({
292
292
  startTime: timingInfo.startTime
@@ -10,7 +10,8 @@ const {
10
10
  isValidHTTPToken,
11
11
  sameOrigin,
12
12
  normalizeMethod,
13
- makePolicyContainer
13
+ makePolicyContainer,
14
+ normalizeMethodRecord
14
15
  } = require('./util')
15
16
  const {
16
17
  forbiddenMethodsSet,
@@ -27,13 +28,12 @@ const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols')
27
28
  const { webidl } = require('./webidl')
28
29
  const { getGlobalOrigin } = require('./global')
29
30
  const { URLSerializer } = require('./dataURL')
30
- const { kHeadersList } = require('../core/symbols')
31
+ const { kHeadersList, kConstruct } = require('../core/symbols')
31
32
  const assert = require('assert')
32
33
  const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events')
33
34
 
34
35
  let TransformStream = globalThis.TransformStream
35
36
 
36
- const kInit = Symbol('init')
37
37
  const kAbortController = Symbol('abortController')
38
38
 
39
39
  const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
@@ -44,7 +44,7 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
44
44
  class Request {
45
45
  // https://fetch.spec.whatwg.org/#dom-request
46
46
  constructor (input, init = {}) {
47
- if (input === kInit) {
47
+ if (input === kConstruct) {
48
48
  return
49
49
  }
50
50
 
@@ -183,8 +183,10 @@ class Request {
183
183
  urlList: [...request.urlList]
184
184
  })
185
185
 
186
+ const initHasKey = Object.keys(init).length !== 0
187
+
186
188
  // 13. If init is not empty, then:
187
- if (Object.keys(init).length > 0) {
189
+ if (initHasKey) {
188
190
  // 1. If request’s mode is "navigate", then set it to "same-origin".
189
191
  if (request.mode === 'navigate') {
190
192
  request.mode = 'same-origin'
@@ -299,7 +301,7 @@ class Request {
299
301
  }
300
302
 
301
303
  // 23. If init["integrity"] exists, then set request’s integrity metadata to it.
302
- if (init.integrity !== undefined && init.integrity != null) {
304
+ if (init.integrity != null) {
303
305
  request.integrity = String(init.integrity)
304
306
  }
305
307
 
@@ -315,16 +317,16 @@ class Request {
315
317
 
316
318
  // 2. If method is not a method or method is a forbidden method, then
317
319
  // throw a TypeError.
318
- if (!isValidHTTPToken(init.method)) {
319
- throw new TypeError(`'${init.method}' is not a valid HTTP method.`)
320
+ if (!isValidHTTPToken(method)) {
321
+ throw new TypeError(`'${method}' is not a valid HTTP method.`)
320
322
  }
321
323
 
322
324
  if (forbiddenMethodsSet.has(method.toUpperCase())) {
323
- throw new TypeError(`'${init.method}' HTTP method is unsupported.`)
325
+ throw new TypeError(`'${method}' HTTP method is unsupported.`)
324
326
  }
325
327
 
326
328
  // 3. Normalize method.
327
- method = normalizeMethod(init.method)
329
+ method = normalizeMethodRecord[method] ?? normalizeMethod(method)
328
330
 
329
331
  // 4. Set request’s method to method.
330
332
  request.method = method
@@ -395,7 +397,7 @@ class Request {
395
397
  // 30. Set this’s headers to a new Headers object with this’s relevant
396
398
  // Realm, whose header list is request’s header list and guard is
397
399
  // "request".
398
- this[kHeaders] = new Headers()
400
+ this[kHeaders] = new Headers(kConstruct)
399
401
  this[kHeaders][kHeadersList] = request.headersList
400
402
  this[kHeaders][kGuard] = 'request'
401
403
  this[kHeaders][kRealm] = this[kRealm]
@@ -415,25 +417,25 @@ class Request {
415
417
  }
416
418
 
417
419
  // 32. If init is not empty, then:
418
- if (Object.keys(init).length !== 0) {
420
+ if (initHasKey) {
421
+ /** @type {HeadersList} */
422
+ const headersList = this[kHeaders][kHeadersList]
419
423
  // 1. Let headers be a copy of this’s headers and its associated header
420
424
  // list.
421
- let headers = new Headers(this[kHeaders])
422
-
423
425
  // 2. If init["headers"] exists, then set headers to init["headers"].
424
- if (init.headers !== undefined) {
425
- headers = init.headers
426
- }
426
+ const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList)
427
427
 
428
428
  // 3. Empty this’s headers’s header list.
429
- this[kHeaders][kHeadersList].clear()
429
+ headersList.clear()
430
430
 
431
431
  // 4. If headers is a Headers object, then for each header in its header
432
432
  // list, append header’s name/header’s value to this’s headers.
433
- if (headers.constructor.name === 'Headers') {
433
+ if (headers instanceof HeadersList) {
434
434
  for (const [key, val] of headers) {
435
- this[kHeaders].append(key, val)
435
+ headersList.append(key, val)
436
436
  }
437
+ // Note: Copy the `set-cookie` meta-data.
438
+ headersList.cookies = headers.cookies
437
439
  } else {
438
440
  // 5. Otherwise, fill this’s headers with headers.
439
441
  fillHeaders(this[kHeaders], headers)
@@ -722,10 +724,10 @@ class Request {
722
724
 
723
725
  // 3. Let clonedRequestObject be the result of creating a Request object,
724
726
  // given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
725
- const clonedRequestObject = new Request(kInit)
727
+ const clonedRequestObject = new Request(kConstruct)
726
728
  clonedRequestObject[kState] = clonedRequest
727
729
  clonedRequestObject[kRealm] = this[kRealm]
728
- clonedRequestObject[kHeaders] = new Headers()
730
+ clonedRequestObject[kHeaders] = new Headers(kConstruct)
729
731
  clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList
730
732
  clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard]
731
733
  clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm]
@@ -23,7 +23,7 @@ const { webidl } = require('./webidl')
23
23
  const { FormData } = require('./formdata')
24
24
  const { getGlobalOrigin } = require('./global')
25
25
  const { URLSerializer } = require('./dataURL')
26
- const { kHeadersList } = require('../core/symbols')
26
+ const { kHeadersList, kConstruct } = require('../core/symbols')
27
27
  const assert = require('assert')
28
28
  const { types } = require('util')
29
29
 
@@ -144,7 +144,7 @@ class Response {
144
144
  // 2. Set this’s headers to a new Headers object with this’s relevant
145
145
  // Realm, whose header list is this’s response’s header list and guard
146
146
  // is "response".
147
- this[kHeaders] = new Headers()
147
+ this[kHeaders] = new Headers(kConstruct)
148
148
  this[kHeaders][kGuard] = 'response'
149
149
  this[kHeaders][kHeadersList] = this[kState].headersList
150
150
  this[kHeaders][kRealm] = this[kRealm]
@@ -514,11 +514,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V) {
514
514
  return webidl.converters.Blob(V, { strict: false })
515
515
  }
516
516
 
517
- if (
518
- types.isAnyArrayBuffer(V) ||
519
- types.isTypedArray(V) ||
520
- types.isDataView(V)
521
- ) {
517
+ if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) {
522
518
  return webidl.converters.BufferSource(V)
523
519
  }
524
520
 
package/lib/fetch/util.js CHANGED
@@ -698,11 +698,30 @@ function isCancelled (fetchParams) {
698
698
  fetchParams.controller.state === 'terminated'
699
699
  }
700
700
 
701
- // https://fetch.spec.whatwg.org/#concept-method-normalize
701
+ const normalizeMethodRecord = {
702
+ delete: 'DELETE',
703
+ DELETE: 'DELETE',
704
+ get: 'GET',
705
+ GET: 'GET',
706
+ head: 'HEAD',
707
+ HEAD: 'HEAD',
708
+ options: 'OPTIONS',
709
+ OPTIONS: 'OPTIONS',
710
+ post: 'POST',
711
+ POST: 'POST',
712
+ put: 'PUT',
713
+ PUT: 'PUT'
714
+ }
715
+
716
+ // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
717
+ Object.setPrototypeOf(normalizeMethodRecord, null)
718
+
719
+ /**
720
+ * @see https://fetch.spec.whatwg.org/#concept-method-normalize
721
+ * @param {string} method
722
+ */
702
723
  function normalizeMethod (method) {
703
- return /^(DELETE|GET|HEAD|OPTIONS|POST|PUT)$/i.test(method)
704
- ? method.toUpperCase()
705
- : method
724
+ return normalizeMethodRecord[method.toLowerCase()] ?? method
706
725
  }
707
726
 
708
727
  // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
@@ -1047,5 +1066,6 @@ module.exports = {
1047
1066
  urlIsLocal,
1048
1067
  urlHasHttpsScheme,
1049
1068
  urlIsHttpHttpsScheme,
1050
- readAllBytes
1069
+ readAllBytes,
1070
+ normalizeMethodRecord
1051
1071
  }
@@ -1,4 +1,4 @@
1
- const assert = require('node:assert')
1
+ const assert = require('assert')
2
2
 
3
3
  const { kRetryHandlerDefaultRetry } = require('../core/symbols')
4
4
  const { RequestRetryError } = require('../core/errors')
@@ -95,7 +95,7 @@ class RetryHandler {
95
95
  }
96
96
 
97
97
  onBodySent (chunk) {
98
- return this.handler.onBodySent(chunk)
98
+ if (this.handler.onBodySent) return this.handler.onBodySent(chunk)
99
99
  }
100
100
 
101
101
  static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
@@ -65,6 +65,9 @@ class ProxyAgent extends DispatcherBase {
65
65
  this[kProxyTls] = opts.proxyTls
66
66
  this[kProxyHeaders] = opts.headers || {}
67
67
 
68
+ const resolvedUrl = new URL(opts.uri)
69
+ const { origin, port, host, username, password } = resolvedUrl
70
+
68
71
  if (opts.auth && opts.token) {
69
72
  throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
70
73
  } else if (opts.auth) {
@@ -72,11 +75,10 @@ class ProxyAgent extends DispatcherBase {
72
75
  this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
73
76
  } else if (opts.token) {
74
77
  this[kProxyHeaders]['proxy-authorization'] = opts.token
78
+ } else if (username && password) {
79
+ this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
75
80
  }
76
81
 
77
- const resolvedUrl = new URL(opts.uri)
78
- const { origin, port, host } = resolvedUrl
79
-
80
82
  const connect = buildConnector({ ...opts.proxyTls })
81
83
  this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
82
84
  this[kClient] = clientFactory(resolvedUrl, { connect })
@@ -100,7 +102,7 @@ class ProxyAgent extends DispatcherBase {
100
102
  })
101
103
  if (statusCode !== 200) {
102
104
  socket.on('error', () => {}).destroy()
103
- callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling'))
105
+ callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
104
106
  }
105
107
  if (opts.protocol !== 'https:') {
106
108
  callback(null, socket)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "5.28.0",
3
+ "version": "5.28.2",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -110,12 +110,12 @@
110
110
  "dns-packet": "^5.4.0",
111
111
  "docsify-cli": "^4.4.3",
112
112
  "form-data": "^4.0.0",
113
- "formdata-node": "^4.3.1",
113
+ "formdata-node": "^6.0.3",
114
114
  "https-pem": "^3.0.0",
115
115
  "husky": "^8.0.1",
116
116
  "import-fresh": "^3.3.0",
117
117
  "jest": "^29.0.2",
118
- "jsdom": "^22.1.0",
118
+ "jsdom": "^23.0.0",
119
119
  "jsfuzz": "^1.0.15",
120
120
  "mocha": "^10.0.0",
121
121
  "mockttp": "^3.9.2",
@@ -124,7 +124,7 @@
124
124
  "proxy": "^1.0.2",
125
125
  "proxyquire": "^2.1.3",
126
126
  "semver": "^7.5.4",
127
- "sinon": "^16.1.0",
127
+ "sinon": "^17.0.1",
128
128
  "snazzy": "^9.0.0",
129
129
  "standard": "^17.0.0",
130
130
  "table": "^6.8.0",
package/types/fetch.d.ts CHANGED
@@ -108,7 +108,7 @@ export interface RequestInit {
108
108
  body?: BodyInit
109
109
  redirect?: RequestRedirect
110
110
  integrity?: string
111
- signal?: AbortSignal
111
+ signal?: AbortSignal | null
112
112
  credentials?: RequestCredentials
113
113
  mode?: RequestMode
114
114
  referrer?: string