undici 6.0.0 → 6.1.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.
@@ -9,7 +9,7 @@ const {
9
9
  filterResponse,
10
10
  makeResponse
11
11
  } = require('./response')
12
- const { Headers } = require('./headers')
12
+ const { Headers, HeadersList } = require('./headers')
13
13
  const { Request, makeRequest } = require('./request')
14
14
  const zlib = require('zlib')
15
15
  const {
@@ -41,6 +41,7 @@ const {
41
41
  urlIsLocal,
42
42
  urlIsHttpHttpsScheme,
43
43
  urlHasHttpsScheme,
44
+ clampAndCoursenConnectionTimingInfo,
44
45
  simpleRangeHeaderValue,
45
46
  buildContentRange
46
47
  } = require('./util')
@@ -57,7 +58,7 @@ const {
57
58
  const { kHeadersList, kConstruct } = require('../core/symbols')
58
59
  const EE = require('events')
59
60
  const { Readable, pipeline } = require('stream')
60
- const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor } = require('../core/util')
61
+ const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor, bufferToLowerCasedHeaderName } = require('../core/util')
61
62
  const { dataURLProcessor, serializeAMimeType, parseMIMEType } = require('./dataURL')
62
63
  const { getGlobalDispatcher } = require('../global')
63
64
  const { webidl } = require('./webidl')
@@ -475,7 +476,7 @@ function fetching ({
475
476
  }
476
477
 
477
478
  // 12. If request’s header list does not contain `Accept`, then:
478
- if (!request.headersList.contains('accept')) {
479
+ if (!request.headersList.contains('accept', true)) {
479
480
  // 1. Let value be `*/*`.
480
481
  const value = '*/*'
481
482
 
@@ -492,14 +493,14 @@ function fetching ({
492
493
  // TODO
493
494
 
494
495
  // 3. Append `Accept`/value to request’s header list.
495
- request.headersList.append('accept', value)
496
+ request.headersList.append('accept', value, true)
496
497
  }
497
498
 
498
499
  // 13. If request’s header list does not contain `Accept-Language`, then
499
500
  // user agents should append `Accept-Language`/an appropriate value to
500
501
  // request’s header list.
501
- if (!request.headersList.contains('accept-language')) {
502
- request.headersList.append('accept-language', '*')
502
+ if (!request.headersList.contains('accept-language', true)) {
503
+ request.headersList.append('accept-language', '*', true)
503
504
  }
504
505
 
505
506
  // 14. If request’s priority is null, then use request’s initiator and
@@ -718,7 +719,7 @@ async function mainFetch (fetchParams, recursive = false) {
718
719
  response.type === 'opaque' &&
719
720
  internalResponse.status === 206 &&
720
721
  internalResponse.rangeRequested &&
721
- !request.headers.contains('range')
722
+ !request.headers.contains('range', true)
722
723
  ) {
723
724
  response = internalResponse = makeNetworkError()
724
725
  }
@@ -840,7 +841,7 @@ function schemeFetch (fetchParams) {
840
841
 
841
842
  // 8. If request’s header list does not contain `Range`:
842
843
  // 9. Otherwise:
843
- if (!request.headersList.contains('range')) {
844
+ if (!request.headersList.contains('range', true)) {
844
845
  // 1. Let bodyWithType be the result of safely extracting blob.
845
846
  // Note: in the FileAPI a blob "object" is a Blob *or* a MediaSource.
846
847
  // In node, this can only ever be a Blob. Therefore we can safely
@@ -854,14 +855,14 @@ function schemeFetch (fetchParams) {
854
855
  response.body = bodyWithType[0]
855
856
 
856
857
  // 4. Set response’s header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ».
857
- response.headersList.set('content-length', serializedFullLength)
858
- response.headersList.set('content-type', type)
858
+ response.headersList.set('content-length', serializedFullLength, true)
859
+ response.headersList.set('content-type', type, true)
859
860
  } else {
860
861
  // 1. Set response’s range-requested flag.
861
862
  response.rangeRequested = true
862
863
 
863
864
  // 2. Let rangeHeader be the result of getting `Range` from request’s header list.
864
- const rangeHeader = request.headersList.get('range')
865
+ const rangeHeader = request.headersList.get('range', true)
865
866
 
866
867
  // 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true.
867
868
  const rangeValue = simpleRangeHeaderValue(rangeHeader, true)
@@ -921,9 +922,9 @@ function schemeFetch (fetchParams) {
921
922
 
922
923
  // 15. Set response’s header list to « (`Content-Length`, serializedSlicedLength),
923
924
  // (`Content-Type`, type), (`Content-Range`, contentRange) ».
924
- response.headersList.set('content-length', serializedSlicedLength)
925
- response.headersList.set('content-type', type)
926
- response.headersList.set('content-range', contentRange)
925
+ response.headersList.set('content-length', serializedSlicedLength, true)
926
+ response.headersList.set('content-type', type, true)
927
+ response.headersList.set('content-range', contentRange, true)
927
928
  }
928
929
 
929
930
  // 10. Return response.
@@ -1040,7 +1041,7 @@ function fetchFinale (fetchParams, response) {
1040
1041
  responseStatus = response.status
1041
1042
 
1042
1043
  // 2. Let mimeType be the result of extracting a MIME type from response’s header list.
1043
- const mimeType = parseMIMEType(response.headersList.get('content-type')) // TODO: fix
1044
+ const mimeType = parseMIMEType(response.headersList.get('content-type', true)) // TODO: fix
1044
1045
 
1045
1046
  // 3. If mimeType is not failure, then set bodyInfo’s content type to the result of minimizing a supported MIME type given mimeType.
1046
1047
  if (mimeType !== 'failure') {
@@ -1336,11 +1337,11 @@ function httpRedirectFetch (fetchParams, response) {
1336
1337
  // delete headerName from request’s header list.
1337
1338
  if (!sameOrigin(requestCurrentURL(request), locationURL)) {
1338
1339
  // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
1339
- request.headersList.delete('authorization')
1340
+ request.headersList.delete('authorization', true)
1340
1341
 
1341
1342
  // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
1342
- request.headersList.delete('cookie')
1343
- request.headersList.delete('host')
1343
+ request.headersList.delete('cookie', true)
1344
+ request.headersList.delete('host', true)
1344
1345
  }
1345
1346
 
1346
1347
  // 14. If request’s body is non-null, then set request’s body to the first return
@@ -1456,7 +1457,7 @@ async function httpNetworkOrCacheFetch (
1456
1457
  // `Content-Length`/contentLengthHeaderValue to httpRequest’s header
1457
1458
  // list.
1458
1459
  if (contentLengthHeaderValue != null) {
1459
- httpRequest.headersList.append('content-length', contentLengthHeaderValue)
1460
+ httpRequest.headersList.append('content-length', contentLengthHeaderValue, true)
1460
1461
  }
1461
1462
 
1462
1463
  // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`,
@@ -1472,7 +1473,7 @@ async function httpNetworkOrCacheFetch (
1472
1473
  // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded,
1473
1474
  // to httpRequest’s header list.
1474
1475
  if (httpRequest.referrer instanceof URL) {
1475
- httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href))
1476
+ httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href), true)
1476
1477
  }
1477
1478
 
1478
1479
  // 12. Append a request `Origin` header for httpRequest.
@@ -1484,8 +1485,8 @@ async function httpNetworkOrCacheFetch (
1484
1485
  // 14. If httpRequest’s header list does not contain `User-Agent`, then
1485
1486
  // user agents should append `User-Agent`/default `User-Agent` value to
1486
1487
  // httpRequest’s header list.
1487
- if (!httpRequest.headersList.contains('user-agent')) {
1488
- httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node')
1488
+ if (!httpRequest.headersList.contains('user-agent', true)) {
1489
+ httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node', true)
1489
1490
  }
1490
1491
 
1491
1492
  // 15. If httpRequest’s cache mode is "default" and httpRequest’s header
@@ -1494,11 +1495,11 @@ async function httpNetworkOrCacheFetch (
1494
1495
  // httpRequest’s cache mode to "no-store".
1495
1496
  if (
1496
1497
  httpRequest.cache === 'default' &&
1497
- (httpRequest.headersList.contains('if-modified-since') ||
1498
- httpRequest.headersList.contains('if-none-match') ||
1499
- httpRequest.headersList.contains('if-unmodified-since') ||
1500
- httpRequest.headersList.contains('if-match') ||
1501
- httpRequest.headersList.contains('if-range'))
1498
+ (httpRequest.headersList.contains('if-modified-since', true) ||
1499
+ httpRequest.headersList.contains('if-none-match', true) ||
1500
+ httpRequest.headersList.contains('if-unmodified-since', true) ||
1501
+ httpRequest.headersList.contains('if-match', true) ||
1502
+ httpRequest.headersList.contains('if-range', true))
1502
1503
  ) {
1503
1504
  httpRequest.cache = 'no-store'
1504
1505
  }
@@ -1510,44 +1511,44 @@ async function httpNetworkOrCacheFetch (
1510
1511
  if (
1511
1512
  httpRequest.cache === 'no-cache' &&
1512
1513
  !httpRequest.preventNoCacheCacheControlHeaderModification &&
1513
- !httpRequest.headersList.contains('cache-control')
1514
+ !httpRequest.headersList.contains('cache-control', true)
1514
1515
  ) {
1515
- httpRequest.headersList.append('cache-control', 'max-age=0')
1516
+ httpRequest.headersList.append('cache-control', 'max-age=0', true)
1516
1517
  }
1517
1518
 
1518
1519
  // 17. If httpRequest’s cache mode is "no-store" or "reload", then:
1519
1520
  if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') {
1520
1521
  // 1. If httpRequest’s header list does not contain `Pragma`, then append
1521
1522
  // `Pragma`/`no-cache` to httpRequest’s header list.
1522
- if (!httpRequest.headersList.contains('pragma')) {
1523
- httpRequest.headersList.append('pragma', 'no-cache')
1523
+ if (!httpRequest.headersList.contains('pragma', true)) {
1524
+ httpRequest.headersList.append('pragma', 'no-cache', true)
1524
1525
  }
1525
1526
 
1526
1527
  // 2. If httpRequest’s header list does not contain `Cache-Control`,
1527
1528
  // then append `Cache-Control`/`no-cache` to httpRequest’s header list.
1528
- if (!httpRequest.headersList.contains('cache-control')) {
1529
- httpRequest.headersList.append('cache-control', 'no-cache')
1529
+ if (!httpRequest.headersList.contains('cache-control', true)) {
1530
+ httpRequest.headersList.append('cache-control', 'no-cache', true)
1530
1531
  }
1531
1532
  }
1532
1533
 
1533
1534
  // 18. If httpRequest’s header list contains `Range`, then append
1534
1535
  // `Accept-Encoding`/`identity` to httpRequest’s header list.
1535
- if (httpRequest.headersList.contains('range')) {
1536
- httpRequest.headersList.append('accept-encoding', 'identity')
1536
+ if (httpRequest.headersList.contains('range', true)) {
1537
+ httpRequest.headersList.append('accept-encoding', 'identity', true)
1537
1538
  }
1538
1539
 
1539
1540
  // 19. Modify httpRequest’s header list per HTTP. Do not append a given
1540
1541
  // header if httpRequest’s header list contains that header’s name.
1541
1542
  // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
1542
- if (!httpRequest.headersList.contains('accept-encoding')) {
1543
+ if (!httpRequest.headersList.contains('accept-encoding', true)) {
1543
1544
  if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
1544
- httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate')
1545
+ httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate', true)
1545
1546
  } else {
1546
- httpRequest.headersList.append('accept-encoding', 'gzip, deflate')
1547
+ httpRequest.headersList.append('accept-encoding', 'gzip, deflate', true)
1547
1548
  }
1548
1549
  }
1549
1550
 
1550
- httpRequest.headersList.delete('host')
1551
+ httpRequest.headersList.delete('host', true)
1551
1552
 
1552
1553
  // 20. If includeCredentials is true, then:
1553
1554
  if (includeCredentials) {
@@ -1630,7 +1631,7 @@ async function httpNetworkOrCacheFetch (
1630
1631
 
1631
1632
  // 12. If httpRequest’s header list contains `Range`, then set response’s
1632
1633
  // range-requested flag.
1633
- if (httpRequest.headersList.contains('range')) {
1634
+ if (httpRequest.headersList.contains('range', true)) {
1634
1635
  response.rangeRequested = true
1635
1636
  }
1636
1637
 
@@ -2075,7 +2076,7 @@ async function httpNetworkFetch (
2075
2076
  // 20. Return response.
2076
2077
  return response
2077
2078
 
2078
- async function dispatch ({ body }) {
2079
+ function dispatch ({ body }) {
2079
2080
  const url = requestCurrentURL(request)
2080
2081
  /** @type {import('../..').Agent} */
2081
2082
  const agent = fetchParams.controller.dispatcher
@@ -2085,7 +2086,7 @@ async function httpNetworkFetch (
2085
2086
  path: url.pathname + url.search,
2086
2087
  origin: url.origin,
2087
2088
  method: request.method,
2088
- body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
2089
+ body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
2089
2090
  headers: request.headersList.entries,
2090
2091
  maxRedirections: 0,
2091
2092
  upgrade: request.mode === 'websocket' ? 'websocket' : undefined
@@ -2098,67 +2099,83 @@ async function httpNetworkFetch (
2098
2099
  // TODO (fix): Do we need connection here?
2099
2100
  const { connection } = fetchParams.controller
2100
2101
 
2102
+ // Set timingInfo’s final connection timing info to the result of calling clamp and coarsen
2103
+ // connection timing info with connection’s timing info, timingInfo’s post-redirect start
2104
+ // time, and fetchParams’s cross-origin isolated capability.
2105
+ // TODO: implement connection timing
2106
+ timingInfo.finalConnectionTimingInfo = clampAndCoursenConnectionTimingInfo(undefined, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability)
2107
+
2101
2108
  if (connection.destroyed) {
2102
2109
  abort(new DOMException('The operation was aborted.', 'AbortError'))
2103
2110
  } else {
2104
2111
  fetchParams.controller.on('terminated', abort)
2105
2112
  this.abort = connection.abort = abort
2106
2113
  }
2114
+
2115
+ // Set timingInfo’s final network-request start time to the coarsened shared current time given
2116
+ // fetchParams’s cross-origin isolated capability.
2117
+ timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
2118
+ },
2119
+
2120
+ onResponseStarted () {
2121
+ // Set timingInfo’s final network-response start time to the coarsened shared current
2122
+ // time given fetchParams’s cross-origin isolated capability, immediately after the
2123
+ // user agent’s HTTP parser receives the first byte of the response (e.g., frame header
2124
+ // bytes for HTTP/2 or response status line for HTTP/1.x).
2125
+ timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
2107
2126
  },
2108
2127
 
2109
- onHeaders (status, headersList, resume, statusText) {
2128
+ onHeaders (status, rawHeaders, resume, statusText) {
2110
2129
  if (status < 200) {
2111
2130
  return
2112
2131
  }
2113
2132
 
2133
+ /** @type {string[]} */
2114
2134
  let codings = []
2115
2135
  let location = ''
2116
2136
 
2117
- const headers = new Headers()
2137
+ const headersList = new HeadersList()
2118
2138
 
2119
- // For H2, the headers are a plain JS object
2139
+ // For H2, the rawHeaders are a plain JS object
2120
2140
  // We distinguish between them and iterate accordingly
2121
- if (Array.isArray(headersList)) {
2122
- for (let n = 0; n < headersList.length; n += 2) {
2123
- const key = headersList[n + 0].toString('latin1')
2124
- const val = headersList[n + 1].toString('latin1')
2125
- if (key.toLowerCase() === 'content-encoding') {
2126
- // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2127
- // "All content-coding values are case-insensitive..."
2128
- codings = val.toLowerCase().split(',').map((x) => x.trim())
2129
- } else if (key.toLowerCase() === 'location') {
2130
- location = val
2131
- }
2132
-
2133
- headers[kHeadersList].append(key, val)
2141
+ if (Array.isArray(rawHeaders)) {
2142
+ for (let i = 0; i < rawHeaders.length; i += 2) {
2143
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
2134
2144
  }
2145
+ const contentEncoding = headersList.get('content-encoding', true)
2146
+ if (contentEncoding) {
2147
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2148
+ // "All content-coding values are case-insensitive..."
2149
+ codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
2150
+ }
2151
+ location = headersList.get('location', true)
2135
2152
  } else {
2136
- const keys = Object.keys(headersList)
2137
- for (const key of keys) {
2138
- const val = headersList[key]
2139
- if (key.toLowerCase() === 'content-encoding') {
2140
- // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2141
- // "All content-coding values are case-insensitive..."
2142
- codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse()
2143
- } else if (key.toLowerCase() === 'location') {
2144
- location = val
2145
- }
2146
-
2147
- headers[kHeadersList].append(key, val)
2153
+ const keys = Object.keys(rawHeaders)
2154
+ for (let i = 0; i < keys.length; ++i) {
2155
+ headersList.append(keys[i], rawHeaders[keys[i]])
2148
2156
  }
2157
+ // For H2, The header names are already in lowercase,
2158
+ // so we can avoid the `HeadersList#get` call here.
2159
+ const contentEncoding = rawHeaders['content-encoding']
2160
+ if (contentEncoding) {
2161
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2162
+ // "All content-coding values are case-insensitive..."
2163
+ codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()).reverse()
2164
+ }
2165
+ location = rawHeaders.location
2149
2166
  }
2150
2167
 
2151
2168
  this.body = new Readable({ read: resume })
2152
2169
 
2153
2170
  const decoders = []
2154
2171
 
2155
- const willFollow = request.redirect === 'follow' &&
2156
- location &&
2172
+ const willFollow = location && request.redirect === 'follow' &&
2157
2173
  redirectStatusSet.has(status)
2158
2174
 
2159
2175
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2160
2176
  if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2161
- for (const coding of codings) {
2177
+ for (let i = 0; i < codings.length; ++i) {
2178
+ const coding = codings[i]
2162
2179
  // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
2163
2180
  if (coding === 'x-gzip' || coding === 'gzip') {
2164
2181
  decoders.push(zlib.createGunzip({
@@ -2183,7 +2200,7 @@ async function httpNetworkFetch (
2183
2200
  resolve({
2184
2201
  status,
2185
2202
  statusText,
2186
- headersList: headers[kHeadersList],
2203
+ headersList,
2187
2204
  body: decoders.length
2188
2205
  ? pipeline(this.body, ...decoders, () => { })
2189
2206
  : this.body.on('error', () => {})
@@ -2237,24 +2254,21 @@ async function httpNetworkFetch (
2237
2254
  reject(error)
2238
2255
  },
2239
2256
 
2240
- onUpgrade (status, headersList, socket) {
2257
+ onUpgrade (status, rawHeaders, socket) {
2241
2258
  if (status !== 101) {
2242
2259
  return
2243
2260
  }
2244
2261
 
2245
- const headers = new Headers()
2246
-
2247
- for (let n = 0; n < headersList.length; n += 2) {
2248
- const key = headersList[n + 0].toString('latin1')
2249
- const val = headersList[n + 1].toString('latin1')
2262
+ const headersList = new HeadersList()
2250
2263
 
2251
- headers[kHeadersList].append(key, val)
2264
+ for (let i = 0; i < rawHeaders.length; i += 2) {
2265
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
2252
2266
  }
2253
2267
 
2254
2268
  resolve({
2255
2269
  status,
2256
2270
  statusText: STATUS_CODES[status],
2257
- headersList: headers[kHeadersList],
2271
+ headersList,
2258
2272
  socket
2259
2273
  })
2260
2274
 
@@ -126,7 +126,7 @@ class Response {
126
126
  const value = isomorphicEncode(URLSerializer(parsedURL))
127
127
 
128
128
  // 7. Append `Location`/value to responseObject’s response’s header list.
129
- responseObject[kState].headersList.append('location', value)
129
+ responseObject[kState].headersList.append('location', value, true)
130
130
 
131
131
  // 8. Return responseObject.
132
132
  return responseObject
@@ -496,8 +496,8 @@ function initializeResponse (response, init, body) {
496
496
 
497
497
  // 3. If body's type is non-null and response's header list does not contain
498
498
  // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
499
- if (body.type != null && !response[kState].headersList.contains('Content-Type')) {
500
- response[kState].headersList.append('content-type', body.type)
499
+ if (body.type != null && !response[kState].headersList.contains('content-type', true)) {
500
+ response[kState].headersList.append('content-type', body.type, true)
501
501
  }
502
502
  }
503
503
  }
package/lib/fetch/util.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
4
4
  const { getGlobalOrigin } = require('./global')
5
5
  const { performance } = require('perf_hooks')
6
- const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
6
+ const { isBlobLike, toUSVString, ReadableStreamFrom, isValidHTTPToken } = require('../core/util')
7
7
  const assert = require('assert')
8
8
  const { isUint8Array } = require('util/types')
9
9
 
@@ -35,7 +35,7 @@ function responseLocationURL (response, requestFragment) {
35
35
 
36
36
  // 2. Let location be the result of extracting header list values given
37
37
  // `Location` and response’s header list.
38
- let location = response.headersList.get('location')
38
+ let location = response.headersList.get('location', true)
39
39
 
40
40
  // 3. If location is a header value, then set location to the result of
41
41
  // parsing location with response’s URL.
@@ -103,52 +103,6 @@ function isValidReasonPhrase (statusText) {
103
103
  return true
104
104
  }
105
105
 
106
- /**
107
- * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
108
- * @param {number} c
109
- */
110
- function isTokenCharCode (c) {
111
- switch (c) {
112
- case 0x22:
113
- case 0x28:
114
- case 0x29:
115
- case 0x2c:
116
- case 0x2f:
117
- case 0x3a:
118
- case 0x3b:
119
- case 0x3c:
120
- case 0x3d:
121
- case 0x3e:
122
- case 0x3f:
123
- case 0x40:
124
- case 0x5b:
125
- case 0x5c:
126
- case 0x5d:
127
- case 0x7b:
128
- case 0x7d:
129
- // DQUOTE and "(),/:;<=>?@[\]{}"
130
- return false
131
- default:
132
- // VCHAR %x21-7E
133
- return c >= 0x21 && c <= 0x7e
134
- }
135
- }
136
-
137
- /**
138
- * @param {string} characters
139
- */
140
- function isValidHTTPToken (characters) {
141
- if (characters.length === 0) {
142
- return false
143
- }
144
- for (let i = 0; i < characters.length; ++i) {
145
- if (!isTokenCharCode(characters.charCodeAt(i))) {
146
- return false
147
- }
148
- }
149
- return true
150
- }
151
-
152
106
  /**
153
107
  * @see https://fetch.spec.whatwg.org/#header-name
154
108
  * @param {string} potentialValue
@@ -199,7 +153,7 @@ function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
199
153
  // 2. Let policy be the empty string.
200
154
  // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
201
155
  // 4. Return policy.
202
- const policyHeader = (headersList.get('referrer-policy') ?? '').split(',')
156
+ const policyHeader = (headersList.get('referrer-policy', true) ?? '').split(',')
203
157
 
204
158
  // Note: As the referrer-policy can contain multiple policies
205
159
  // separated by comma, we need to loop through all of them
@@ -258,7 +212,7 @@ function appendFetchMetadata (httpRequest) {
258
212
  header = httpRequest.mode
259
213
 
260
214
  // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
261
- httpRequest.headersList.set('sec-fetch-mode', header)
215
+ httpRequest.headersList.set('sec-fetch-mode', header, true)
262
216
 
263
217
  // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
264
218
  // TODO
@@ -275,7 +229,7 @@ function appendRequestOriginHeader (request) {
275
229
  // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list.
276
230
  if (request.responseTainting === 'cors' || request.mode === 'websocket') {
277
231
  if (serializedOrigin) {
278
- request.headersList.append('origin', serializedOrigin)
232
+ request.headersList.append('origin', serializedOrigin, true)
279
233
  }
280
234
 
281
235
  // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
@@ -306,14 +260,43 @@ function appendRequestOriginHeader (request) {
306
260
 
307
261
  if (serializedOrigin) {
308
262
  // 2. Append (`Origin`, serializedOrigin) to request’s header list.
309
- request.headersList.append('origin', serializedOrigin)
263
+ request.headersList.append('origin', serializedOrigin, true)
310
264
  }
311
265
  }
312
266
  }
313
267
 
314
- function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
268
+ // https://w3c.github.io/hr-time/#dfn-coarsen-time
269
+ function coarsenTime (timestamp, crossOriginIsolatedCapability) {
315
270
  // TODO
316
- return performance.now()
271
+ return timestamp
272
+ }
273
+
274
+ // https://fetch.spec.whatwg.org/#clamp-and-coarsen-connection-timing-info
275
+ function clampAndCoursenConnectionTimingInfo (connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) {
276
+ if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) {
277
+ return {
278
+ domainLookupStartTime: defaultStartTime,
279
+ domainLookupEndTime: defaultStartTime,
280
+ connectionStartTime: defaultStartTime,
281
+ connectionEndTime: defaultStartTime,
282
+ secureConnectionStartTime: defaultStartTime,
283
+ ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol
284
+ }
285
+ }
286
+
287
+ return {
288
+ domainLookupStartTime: coarsenTime(connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability),
289
+ domainLookupEndTime: coarsenTime(connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability),
290
+ connectionStartTime: coarsenTime(connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability),
291
+ connectionEndTime: coarsenTime(connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability),
292
+ secureConnectionStartTime: coarsenTime(connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability),
293
+ ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol
294
+ }
295
+ }
296
+
297
+ // https://w3c.github.io/hr-time/#dfn-coarsened-shared-current-time
298
+ function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
299
+ return coarsenTime(performance.now(), crossOriginIsolatedCapability)
317
300
  }
318
301
 
319
302
  // https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
@@ -897,22 +880,27 @@ function isReadableStreamLike (stream) {
897
880
  )
898
881
  }
899
882
 
900
- const MAXIMUM_ARGUMENT_LENGTH = 65535
901
-
902
883
  /**
903
884
  * @see https://infra.spec.whatwg.org/#isomorphic-decode
904
- * @param {number[]|Uint8Array} input
885
+ * @param {Uint8Array} input
905
886
  */
906
887
  function isomorphicDecode (input) {
907
888
  // 1. To isomorphic decode a byte sequence input, return a string whose code point
908
889
  // length is equal to input’s length and whose code points have the same values
909
890
  // as the values of input’s bytes, in the same order.
910
-
911
- if (input.length < MAXIMUM_ARGUMENT_LENGTH) {
912
- return String.fromCharCode(...input)
891
+ const length = input.length
892
+ if ((2 << 15) - 1 > length) {
893
+ return String.fromCharCode.apply(null, input)
913
894
  }
914
-
915
- return input.reduce((previous, current) => previous + String.fromCharCode(current), '')
895
+ let result = ''; let i = 0
896
+ let addition = (2 << 15) - 1
897
+ while (i < length) {
898
+ if (i + addition > length) {
899
+ addition = length - i
900
+ }
901
+ result += String.fromCharCode.apply(null, input.subarray(i, i += addition))
902
+ }
903
+ return result
916
904
  }
917
905
 
918
906
  /**
@@ -1186,6 +1174,7 @@ module.exports = {
1186
1174
  ReadableStreamFrom,
1187
1175
  toUSVString,
1188
1176
  tryUpgradeRequestToAPotentiallyTrustworthyURL,
1177
+ clampAndCoursenConnectionTimingInfo,
1189
1178
  coarsenedSharedCurrentTime,
1190
1179
  determineRequestsReferrer,
1191
1180
  makePolicyContainer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.0.0",
3
+ "version": "6.1.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": {
@@ -117,6 +117,7 @@
117
117
  "jest": "^29.0.2",
118
118
  "jsdom": "^23.0.0",
119
119
  "jsfuzz": "^1.0.15",
120
+ "mitata": "^0.1.6",
120
121
  "mocha": "^10.0.0",
121
122
  "mockttp": "^3.9.2",
122
123
  "p-timeout": "^3.2.0",
@@ -210,6 +210,8 @@ declare namespace Dispatcher {
210
210
  onError?(err: Error): void;
211
211
  /** Invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method. */
212
212
  onUpgrade?(statusCode: number, headers: Buffer[] | string[] | null, socket: Duplex): void;
213
+ /** Invoked when response is received, before headers have been read. **/
214
+ onResponseStarted?(): void;
213
215
  /** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */
214
216
  onHeaders?(statusCode: number, headers: Buffer[] | string[] | null, resume: () => void, statusText: string): boolean;
215
217
  /** Invoked when response payload data is received. */
package/types/index.d.ts CHANGED
@@ -17,6 +17,7 @@ import ProxyAgent from'./proxy-agent'
17
17
  import RetryHandler from'./retry-handler'
18
18
  import { request, pipeline, stream, connect, upgrade } from './api'
19
19
 
20
+ export * from './util'
20
21
  export * from './cookies'
21
22
  export * from './fetch'
22
23
  export * from './file'