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.
- package/docs/api/Dispatcher.md +1 -0
- package/docs/api/Util.md +25 -0
- package/index.js +4 -0
- package/lib/agent.js +2 -0
- package/lib/api/readable.js +87 -70
- package/lib/client.js +24 -13
- package/lib/core/connect.js +1 -1
- package/lib/core/constants.js +2 -0
- package/lib/core/errors.js +10 -20
- package/lib/core/request.js +23 -49
- package/lib/core/tree.js +134 -0
- package/lib/core/util.js +86 -33
- package/lib/fetch/body.js +1 -1
- package/lib/fetch/dataURL.js +49 -44
- package/lib/fetch/headers.js +45 -27
- package/lib/fetch/index.js +96 -82
- package/lib/fetch/response.js +3 -3
- package/lib/fetch/util.js +51 -62
- package/package.json +2 -1
- package/types/dispatcher.d.ts +2 -0
- package/types/index.d.ts +1 -0
- package/types/util.d.ts +31 -0
package/lib/fetch/index.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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,
|
|
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
|
|
2137
|
+
const headersList = new HeadersList()
|
|
2118
2138
|
|
|
2119
|
-
// For H2, the
|
|
2139
|
+
// For H2, the rawHeaders are a plain JS object
|
|
2120
2140
|
// We distinguish between them and iterate accordingly
|
|
2121
|
-
if (Array.isArray(
|
|
2122
|
-
for (let
|
|
2123
|
-
|
|
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(
|
|
2137
|
-
for (
|
|
2138
|
-
|
|
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 (
|
|
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
|
|
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,
|
|
2257
|
+
onUpgrade (status, rawHeaders, socket) {
|
|
2241
2258
|
if (status !== 101) {
|
|
2242
2259
|
return
|
|
2243
2260
|
}
|
|
2244
2261
|
|
|
2245
|
-
const
|
|
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
|
-
|
|
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
|
|
2271
|
+
headersList,
|
|
2258
2272
|
socket
|
|
2259
2273
|
})
|
|
2260
2274
|
|
package/lib/fetch/response.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
268
|
+
// https://w3c.github.io/hr-time/#dfn-coarsen-time
|
|
269
|
+
function coarsenTime (timestamp, crossOriginIsolatedCapability) {
|
|
315
270
|
// TODO
|
|
316
|
-
return
|
|
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 {
|
|
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 (
|
|
912
|
-
return String.fromCharCode(
|
|
891
|
+
const length = input.length
|
|
892
|
+
if ((2 << 15) - 1 > length) {
|
|
893
|
+
return String.fromCharCode.apply(null, input)
|
|
913
894
|
}
|
|
914
|
-
|
|
915
|
-
|
|
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.
|
|
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",
|
package/types/dispatcher.d.ts
CHANGED
|
@@ -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'
|