undici 5.6.1 → 5.8.1
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/README.md +1 -1
- package/docs/api/MockAgent.md +2 -2
- package/docs/best-practices/mocking-request.md +9 -9
- package/docs/best-practices/proxy.md +8 -7
- package/index.js +1 -1
- package/lib/balanced-pool.js +81 -4
- package/lib/core/connect.js +28 -5
- package/lib/core/request.js +31 -2
- package/lib/fetch/body.js +106 -31
- package/lib/fetch/constants.js +1 -1
- package/lib/fetch/dataURL.js +1 -1
- package/lib/fetch/headers.js +1 -0
- package/lib/fetch/index.js +11 -7
- package/lib/fetch/request.js +5 -5
- package/lib/fetch/response.js +8 -7
- package/lib/fetch/util.js +15 -1
- package/lib/fetch/webidl.js +7 -5
- package/lib/handler/redirect.js +2 -1
- package/lib/llhttp/llhttp.wasm +0 -0
- package/lib/llhttp/llhttp.wasm.js +1 -1
- package/lib/llhttp/llhttp_simd.wasm +0 -0
- package/lib/llhttp/llhttp_simd.wasm.js +1 -1
- package/lib/mock/mock-interceptor.js +3 -3
- package/lib/mock/mock-utils.js +22 -10
- package/package.json +2 -1
- package/types/mock-interceptor.d.ts +3 -3
- package/types/pool-stats.d.ts +19 -0
- package/types/pool.d.ts +4 -0
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
|
|
|
176
176
|
* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
|
|
177
177
|
* https://fetch.spec.whatwg.org/#fetch-method
|
|
178
178
|
|
|
179
|
-
Only supported on Node 16.
|
|
179
|
+
Only supported on Node 16.8+.
|
|
180
180
|
|
|
181
181
|
This is [experimental](https://nodejs.org/api/documentation.html#documentation_stability_index) and is not yet fully compliant with the Fetch Standard.
|
|
182
182
|
We plan to ship breaking changes to this feature until it is out of experimental.
|
package/docs/api/MockAgent.md
CHANGED
|
@@ -465,7 +465,7 @@ agent.disableNetConnect()
|
|
|
465
465
|
agent
|
|
466
466
|
.get('https://example.com')
|
|
467
467
|
.intercept({ method: 'GET', path: '/' })
|
|
468
|
-
.reply(200
|
|
468
|
+
.reply(200)
|
|
469
469
|
|
|
470
470
|
const pendingInterceptors = agent.pendingInterceptors()
|
|
471
471
|
// Returns [
|
|
@@ -508,7 +508,7 @@ agent.disableNetConnect()
|
|
|
508
508
|
agent
|
|
509
509
|
.get('https://example.com')
|
|
510
510
|
.intercept({ method: 'GET', path: '/' })
|
|
511
|
-
.reply(200
|
|
511
|
+
.reply(200)
|
|
512
512
|
|
|
513
513
|
agent.assertNoPendingInterceptors()
|
|
514
514
|
// Throws an UndiciError with the following message:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Mocking Request
|
|
2
2
|
|
|
3
|
-
Undici
|
|
3
|
+
Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
|
|
4
4
|
|
|
5
5
|
Example:
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Example:
|
|
|
8
8
|
// bank.mjs
|
|
9
9
|
import { request } from 'undici'
|
|
10
10
|
|
|
11
|
-
export async function bankTransfer(
|
|
11
|
+
export async function bankTransfer(recipient, amount) {
|
|
12
12
|
const { body } = await request('http://localhost:3000/bank-transfer',
|
|
13
13
|
{
|
|
14
14
|
method: 'POST',
|
|
@@ -16,7 +16,7 @@ export async function bankTransfer(recepient, amount) {
|
|
|
16
16
|
'X-TOKEN-SECRET': 'SuperSecretToken',
|
|
17
17
|
},
|
|
18
18
|
body: JSON.stringify({
|
|
19
|
-
|
|
19
|
+
recipient,
|
|
20
20
|
amount
|
|
21
21
|
})
|
|
22
22
|
}
|
|
@@ -48,7 +48,7 @@ mockPool.intercept({
|
|
|
48
48
|
'X-TOKEN-SECRET': 'SuperSecretToken',
|
|
49
49
|
},
|
|
50
50
|
body: JSON.stringify({
|
|
51
|
-
|
|
51
|
+
recipient: '1234567890',
|
|
52
52
|
amount: '100'
|
|
53
53
|
})
|
|
54
54
|
}).reply(200, {
|
|
@@ -77,7 +77,7 @@ Explore other MockAgent functionality [here](../api/MockAgent.md)
|
|
|
77
77
|
|
|
78
78
|
## Debug Mock Value
|
|
79
79
|
|
|
80
|
-
When the interceptor
|
|
80
|
+
When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:
|
|
81
81
|
|
|
82
82
|
```js
|
|
83
83
|
const mockAgent = new MockAgent();
|
|
@@ -89,7 +89,7 @@ mockAgent.disableNetConnect()
|
|
|
89
89
|
const mockPool = mockAgent.get('http://localhost:3000');
|
|
90
90
|
|
|
91
91
|
mockPool.intercept({
|
|
92
|
-
path: '/bank-
|
|
92
|
+
path: '/bank-transfer',
|
|
93
93
|
method: 'POST',
|
|
94
94
|
}).reply(200, {
|
|
95
95
|
message: 'transaction processed'
|
|
@@ -103,7 +103,7 @@ const badRequest = await bankTransfer('1234567890', '100')
|
|
|
103
103
|
|
|
104
104
|
## Reply with data based on request
|
|
105
105
|
|
|
106
|
-
If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply
|
|
106
|
+
If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`:
|
|
107
107
|
|
|
108
108
|
```js
|
|
109
109
|
mockPool.intercept({
|
|
@@ -113,7 +113,7 @@ mockPool.intercept({
|
|
|
113
113
|
'X-TOKEN-SECRET': 'SuperSecretToken',
|
|
114
114
|
},
|
|
115
115
|
body: JSON.stringify({
|
|
116
|
-
|
|
116
|
+
recipient: '1234567890',
|
|
117
117
|
amount: '100'
|
|
118
118
|
})
|
|
119
119
|
}).reply(200, (opts) => {
|
|
@@ -129,7 +129,7 @@ in this case opts will be
|
|
|
129
129
|
{
|
|
130
130
|
method: 'POST',
|
|
131
131
|
headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
|
|
132
|
-
body: '{"
|
|
132
|
+
body: '{"recipient":"1234567890","amount":"100"}',
|
|
133
133
|
origin: 'http://localhost:3000',
|
|
134
134
|
path: '/bank-transfer'
|
|
135
135
|
}
|
|
@@ -20,10 +20,10 @@ import { createServer } from 'http'
|
|
|
20
20
|
import proxy from 'proxy'
|
|
21
21
|
|
|
22
22
|
const server = await buildServer()
|
|
23
|
-
const
|
|
23
|
+
const proxyServer = await buildProxy()
|
|
24
24
|
|
|
25
25
|
const serverUrl = `http://localhost:${server.address().port}`
|
|
26
|
-
const proxyUrl = `http://localhost:${
|
|
26
|
+
const proxyUrl = `http://localhost:${proxyServer.address().port}`
|
|
27
27
|
|
|
28
28
|
server.on('request', (req, res) => {
|
|
29
29
|
console.log(req.url) // '/hello?foo=bar'
|
|
@@ -47,7 +47,7 @@ console.log(response.statusCode) // 200
|
|
|
47
47
|
console.log(JSON.parse(data)) // { hello: 'world' }
|
|
48
48
|
|
|
49
49
|
server.close()
|
|
50
|
-
|
|
50
|
+
proxyServer.close()
|
|
51
51
|
client.close()
|
|
52
52
|
|
|
53
53
|
function buildServer () {
|
|
@@ -73,12 +73,12 @@ import { createServer } from 'http'
|
|
|
73
73
|
import proxy from 'proxy'
|
|
74
74
|
|
|
75
75
|
const server = await buildServer()
|
|
76
|
-
const
|
|
76
|
+
const proxyServer = await buildProxy()
|
|
77
77
|
|
|
78
78
|
const serverUrl = `http://localhost:${server.address().port}`
|
|
79
|
-
const proxyUrl = `http://localhost:${
|
|
79
|
+
const proxyUrl = `http://localhost:${proxyServer.address().port}`
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
proxyServer.authenticate = function (req, fn) {
|
|
82
82
|
fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`)
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -107,7 +107,7 @@ console.log(response.statusCode) // 200
|
|
|
107
107
|
console.log(JSON.parse(data)) // { hello: 'world' }
|
|
108
108
|
|
|
109
109
|
server.close()
|
|
110
|
-
|
|
110
|
+
proxyServer.close()
|
|
111
111
|
client.close()
|
|
112
112
|
|
|
113
113
|
function buildServer () {
|
|
@@ -124,3 +124,4 @@ function buildProxy () {
|
|
|
124
124
|
})
|
|
125
125
|
}
|
|
126
126
|
```
|
|
127
|
+
|
package/index.js
CHANGED
|
@@ -80,7 +80,7 @@ function makeDispatcher (fn) {
|
|
|
80
80
|
module.exports.setGlobalDispatcher = setGlobalDispatcher
|
|
81
81
|
module.exports.getGlobalDispatcher = getGlobalDispatcher
|
|
82
82
|
|
|
83
|
-
if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >=
|
|
83
|
+
if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
|
|
84
84
|
let fetchImpl = null
|
|
85
85
|
module.exports.fetch = async function fetch (resource) {
|
|
86
86
|
if (!fetchImpl) {
|
package/lib/balanced-pool.js
CHANGED
|
@@ -18,6 +18,17 @@ const { parseOrigin } = require('./core/util')
|
|
|
18
18
|
const kFactory = Symbol('factory')
|
|
19
19
|
|
|
20
20
|
const kOptions = Symbol('options')
|
|
21
|
+
const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor')
|
|
22
|
+
const kCurrentWeight = Symbol('kCurrentWeight')
|
|
23
|
+
const kIndex = Symbol('kIndex')
|
|
24
|
+
const kWeight = Symbol('kWeight')
|
|
25
|
+
const kMaxWeightPerServer = Symbol('kMaxWeightPerServer')
|
|
26
|
+
const kErrorPenalty = Symbol('kErrorPenalty')
|
|
27
|
+
|
|
28
|
+
function getGreatestCommonDivisor (a, b) {
|
|
29
|
+
if (b === 0) return a
|
|
30
|
+
return getGreatestCommonDivisor(b, a % b)
|
|
31
|
+
}
|
|
21
32
|
|
|
22
33
|
function defaultFactory (origin, opts) {
|
|
23
34
|
return new Pool(origin, opts)
|
|
@@ -28,6 +39,11 @@ class BalancedPool extends PoolBase {
|
|
|
28
39
|
super()
|
|
29
40
|
|
|
30
41
|
this[kOptions] = opts
|
|
42
|
+
this[kIndex] = -1
|
|
43
|
+
this[kCurrentWeight] = 0
|
|
44
|
+
|
|
45
|
+
this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100
|
|
46
|
+
this[kErrorPenalty] = this[kOptions].errorPenalty || 15
|
|
31
47
|
|
|
32
48
|
if (!Array.isArray(upstreams)) {
|
|
33
49
|
upstreams = [upstreams]
|
|
@@ -42,6 +58,7 @@ class BalancedPool extends PoolBase {
|
|
|
42
58
|
for (const upstream of upstreams) {
|
|
43
59
|
this.addUpstream(upstream)
|
|
44
60
|
}
|
|
61
|
+
this._updateBalancedPoolStats()
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
addUpstream (upstream) {
|
|
@@ -54,12 +71,40 @@ class BalancedPool extends PoolBase {
|
|
|
54
71
|
))) {
|
|
55
72
|
return this
|
|
56
73
|
}
|
|
74
|
+
const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))
|
|
75
|
+
|
|
76
|
+
this[kAddClient](pool)
|
|
77
|
+
pool.on('connect', () => {
|
|
78
|
+
pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
pool.on('connectionError', () => {
|
|
82
|
+
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
|
|
83
|
+
this._updateBalancedPoolStats()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
pool.on('disconnect', (...args) => {
|
|
87
|
+
const err = args[2]
|
|
88
|
+
if (err && err.code === 'UND_ERR_SOCKET') {
|
|
89
|
+
// decrease the weight of the pool.
|
|
90
|
+
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
|
|
91
|
+
this._updateBalancedPoolStats()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
for (const client of this[kClients]) {
|
|
96
|
+
client[kWeight] = this[kMaxWeightPerServer]
|
|
97
|
+
}
|
|
57
98
|
|
|
58
|
-
this
|
|
99
|
+
this._updateBalancedPoolStats()
|
|
59
100
|
|
|
60
101
|
return this
|
|
61
102
|
}
|
|
62
103
|
|
|
104
|
+
_updateBalancedPoolStats () {
|
|
105
|
+
this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0)
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
removeUpstream (upstream) {
|
|
64
109
|
const upstreamOrigin = parseOrigin(upstream).origin
|
|
65
110
|
|
|
@@ -100,10 +145,42 @@ class BalancedPool extends PoolBase {
|
|
|
100
145
|
return
|
|
101
146
|
}
|
|
102
147
|
|
|
103
|
-
this[kClients].
|
|
104
|
-
|
|
148
|
+
const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true)
|
|
149
|
+
|
|
150
|
+
if (allClientsBusy) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let counter = 0
|
|
155
|
+
|
|
156
|
+
let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain])
|
|
157
|
+
|
|
158
|
+
while (counter++ < this[kClients].length) {
|
|
159
|
+
this[kIndex] = (this[kIndex] + 1) % this[kClients].length
|
|
160
|
+
const pool = this[kClients][this[kIndex]]
|
|
161
|
+
|
|
162
|
+
// find pool index with the largest weight
|
|
163
|
+
if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) {
|
|
164
|
+
maxWeightIndex = this[kIndex]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// decrease the current weight every `this[kClients].length`.
|
|
168
|
+
if (this[kIndex] === 0) {
|
|
169
|
+
// Set the current weight to the next lower weight.
|
|
170
|
+
this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor]
|
|
171
|
+
|
|
172
|
+
if (this[kCurrentWeight] <= 0) {
|
|
173
|
+
this[kCurrentWeight] = this[kMaxWeightPerServer]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) {
|
|
177
|
+
return pool
|
|
178
|
+
}
|
|
179
|
+
}
|
|
105
180
|
|
|
106
|
-
|
|
181
|
+
this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]
|
|
182
|
+
this[kIndex] = maxWeightIndex
|
|
183
|
+
return this[kClients][maxWeightIndex]
|
|
107
184
|
}
|
|
108
185
|
}
|
|
109
186
|
|
package/lib/core/connect.js
CHANGED
|
@@ -75,14 +75,12 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
75
75
|
})
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const
|
|
79
|
-
? setTimeout(onConnectTimeout, timeout, socket)
|
|
80
|
-
: null
|
|
78
|
+
const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
|
|
81
79
|
|
|
82
80
|
socket
|
|
83
81
|
.setNoDelay(true)
|
|
84
82
|
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
|
|
85
|
-
|
|
83
|
+
cancelTimeout()
|
|
86
84
|
|
|
87
85
|
if (callback) {
|
|
88
86
|
const cb = callback
|
|
@@ -91,7 +89,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
91
89
|
}
|
|
92
90
|
})
|
|
93
91
|
.on('error', function (err) {
|
|
94
|
-
|
|
92
|
+
cancelTimeout()
|
|
95
93
|
|
|
96
94
|
if (callback) {
|
|
97
95
|
const cb = callback
|
|
@@ -104,6 +102,31 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
104
102
|
}
|
|
105
103
|
}
|
|
106
104
|
|
|
105
|
+
function setupTimeout (onConnectTimeout, timeout) {
|
|
106
|
+
if (!timeout) {
|
|
107
|
+
return () => {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let s1 = null
|
|
111
|
+
let s2 = null
|
|
112
|
+
const timeoutId = setTimeout(() => {
|
|
113
|
+
// setImmediate is added to make sure that we priotorise socket error events over timeouts
|
|
114
|
+
s1 = setImmediate(() => {
|
|
115
|
+
if (process.platform === 'win32') {
|
|
116
|
+
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
|
|
117
|
+
s2 = setImmediate(() => onConnectTimeout())
|
|
118
|
+
} else {
|
|
119
|
+
onConnectTimeout()
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}, timeout)
|
|
123
|
+
return () => {
|
|
124
|
+
clearTimeout(timeoutId)
|
|
125
|
+
clearImmediate(s1)
|
|
126
|
+
clearImmediate(s2)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
function onConnectTimeout (socket) {
|
|
108
131
|
util.destroy(socket, new ConnectTimeoutError())
|
|
109
132
|
}
|
package/lib/core/request.js
CHANGED
|
@@ -7,6 +7,27 @@ const {
|
|
|
7
7
|
const assert = require('assert')
|
|
8
8
|
const util = require('./util')
|
|
9
9
|
|
|
10
|
+
// tokenRegExp and headerCharRegex have been lifted from
|
|
11
|
+
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Verifies that the given val is a valid HTTP token
|
|
15
|
+
* per the rules defined in RFC 7230
|
|
16
|
+
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
|
|
17
|
+
*/
|
|
18
|
+
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Matches if val contains an invalid field-vchar
|
|
22
|
+
* field-value = *( field-content / obs-fold )
|
|
23
|
+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
|
24
|
+
* field-vchar = VCHAR / obs-text
|
|
25
|
+
*/
|
|
26
|
+
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
|
|
27
|
+
|
|
28
|
+
// Verifies that a given path is valid does not contain control chars \x00 to \x20
|
|
29
|
+
const invalidPathRegex = /[^\u0021-\u00ff]/
|
|
30
|
+
|
|
10
31
|
const kHandler = Symbol('handler')
|
|
11
32
|
|
|
12
33
|
const channels = {}
|
|
@@ -54,10 +75,14 @@ class Request {
|
|
|
54
75
|
method !== 'CONNECT'
|
|
55
76
|
) {
|
|
56
77
|
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
|
|
78
|
+
} else if (invalidPathRegex.exec(path) !== null) {
|
|
79
|
+
throw new InvalidArgumentError('invalid request path')
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
if (typeof method !== 'string') {
|
|
60
83
|
throw new InvalidArgumentError('method must be a string')
|
|
84
|
+
} else if (tokenRegExp.exec(method) === null) {
|
|
85
|
+
throw new InvalidArgumentError('invalid request method')
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
if (upgrade && typeof upgrade !== 'string') {
|
|
@@ -140,8 +165,8 @@ class Request {
|
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
if (util.isFormDataLike(this.body)) {
|
|
143
|
-
if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor <
|
|
144
|
-
throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.
|
|
168
|
+
if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) {
|
|
169
|
+
throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
if (!extractBody) {
|
|
@@ -301,6 +326,10 @@ function processHeader (request, key, val) {
|
|
|
301
326
|
key.toLowerCase() === 'expect'
|
|
302
327
|
) {
|
|
303
328
|
throw new NotSupportedError('expect header not supported')
|
|
329
|
+
} else if (tokenRegExp.exec(key) === null) {
|
|
330
|
+
throw new InvalidArgumentError('invalid header key')
|
|
331
|
+
} else if (headerCharRegex.exec(val) !== null) {
|
|
332
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
304
333
|
} else {
|
|
305
334
|
request.headers += `${key}: ${val}\r\n`
|
|
306
335
|
}
|
package/lib/fetch/body.js
CHANGED
|
@@ -15,12 +15,7 @@ const { isUint8Array, isArrayBuffer } = require('util/types')
|
|
|
15
15
|
let ReadableStream
|
|
16
16
|
|
|
17
17
|
async function * blobGen (blob) {
|
|
18
|
-
|
|
19
|
-
yield * blob.stream()
|
|
20
|
-
} else {
|
|
21
|
-
// istanbul ignore next: node < 16.7
|
|
22
|
-
yield await blob.arrayBuffer()
|
|
23
|
-
}
|
|
18
|
+
yield * blob.stream()
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
|
|
@@ -263,6 +258,29 @@ function cloneBody (body) {
|
|
|
263
258
|
}
|
|
264
259
|
}
|
|
265
260
|
|
|
261
|
+
async function * consumeBody (body) {
|
|
262
|
+
if (body) {
|
|
263
|
+
if (isUint8Array(body)) {
|
|
264
|
+
yield body
|
|
265
|
+
} else {
|
|
266
|
+
const stream = body.stream
|
|
267
|
+
|
|
268
|
+
if (util.isDisturbed(stream)) {
|
|
269
|
+
throw new TypeError('disturbed')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (stream.locked) {
|
|
273
|
+
throw new TypeError('locked')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Compat.
|
|
277
|
+
stream[kBodyUsed] = true
|
|
278
|
+
|
|
279
|
+
yield * stream
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
266
284
|
function bodyMixinMethods (instance) {
|
|
267
285
|
const methods = {
|
|
268
286
|
async blob () {
|
|
@@ -272,27 +290,14 @@ function bodyMixinMethods (instance) {
|
|
|
272
290
|
|
|
273
291
|
const chunks = []
|
|
274
292
|
|
|
275
|
-
|
|
276
|
-
if (isUint8Array(
|
|
277
|
-
|
|
278
|
-
} else {
|
|
279
|
-
const stream = this[kState].body.stream
|
|
280
|
-
|
|
281
|
-
if (util.isDisturbed(stream)) {
|
|
282
|
-
throw new TypeError('disturbed')
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (stream.locked) {
|
|
286
|
-
throw new TypeError('locked')
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Compat.
|
|
290
|
-
stream[kBodyUsed] = true
|
|
291
|
-
|
|
292
|
-
for await (const chunk of stream) {
|
|
293
|
-
chunks.push(chunk)
|
|
294
|
-
}
|
|
293
|
+
for await (const chunk of consumeBody(this[kState].body)) {
|
|
294
|
+
if (!isUint8Array(chunk)) {
|
|
295
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
295
296
|
}
|
|
297
|
+
|
|
298
|
+
// Assemble one final large blob with Uint8Array's can exhaust memory.
|
|
299
|
+
// That's why we create create multiple blob's and using references
|
|
300
|
+
chunks.push(new Blob([chunk]))
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
|
|
@@ -303,8 +308,54 @@ function bodyMixinMethods (instance) {
|
|
|
303
308
|
throw new TypeError('Illegal invocation')
|
|
304
309
|
}
|
|
305
310
|
|
|
306
|
-
const
|
|
307
|
-
|
|
311
|
+
const contentLength = this.headers.get('content-length')
|
|
312
|
+
const encoded = this.headers.has('content-encoding')
|
|
313
|
+
|
|
314
|
+
// if we have content length and no encoding, then we can
|
|
315
|
+
// pre allocate the buffer and just read the data into it
|
|
316
|
+
if (!encoded && contentLength) {
|
|
317
|
+
const buffer = new Uint8Array(contentLength)
|
|
318
|
+
let offset = 0
|
|
319
|
+
|
|
320
|
+
for await (const chunk of consumeBody(this[kState].body)) {
|
|
321
|
+
if (!isUint8Array(chunk)) {
|
|
322
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
buffer.set(chunk, offset)
|
|
326
|
+
offset += chunk.length
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return buffer.buffer
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// if we don't have content length, then we have to allocate 2x the
|
|
333
|
+
// size of the body, once for consumed data, and once for the final buffer
|
|
334
|
+
|
|
335
|
+
// This could be optimized by using growable ArrayBuffer, but it's not
|
|
336
|
+
// implemented yet. https://github.com/tc39/proposal-resizablearraybuffer
|
|
337
|
+
|
|
338
|
+
const chunks = []
|
|
339
|
+
let size = 0
|
|
340
|
+
|
|
341
|
+
for await (const chunk of consumeBody(this[kState].body)) {
|
|
342
|
+
if (!isUint8Array(chunk)) {
|
|
343
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
chunks.push(chunk)
|
|
347
|
+
size += chunk.byteLength
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const buffer = new Uint8Array(size)
|
|
351
|
+
let offset = 0
|
|
352
|
+
|
|
353
|
+
for (const chunk of chunks) {
|
|
354
|
+
buffer.set(chunk, offset)
|
|
355
|
+
offset += chunk.byteLength
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return buffer.buffer
|
|
308
359
|
},
|
|
309
360
|
|
|
310
361
|
async text () {
|
|
@@ -312,8 +363,21 @@ function bodyMixinMethods (instance) {
|
|
|
312
363
|
throw new TypeError('Illegal invocation')
|
|
313
364
|
}
|
|
314
365
|
|
|
315
|
-
|
|
316
|
-
|
|
366
|
+
let result = ''
|
|
367
|
+
const textDecoder = new TextDecoder()
|
|
368
|
+
|
|
369
|
+
for await (const chunk of consumeBody(this[kState].body)) {
|
|
370
|
+
if (!isUint8Array(chunk)) {
|
|
371
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
result += textDecoder.decode(chunk, { stream: true })
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// flush
|
|
378
|
+
result += textDecoder.decode()
|
|
379
|
+
|
|
380
|
+
return result
|
|
317
381
|
},
|
|
318
382
|
|
|
319
383
|
async json () {
|
|
@@ -340,7 +404,18 @@ function bodyMixinMethods (instance) {
|
|
|
340
404
|
// 1. Let entries be the result of parsing bytes.
|
|
341
405
|
let entries
|
|
342
406
|
try {
|
|
343
|
-
|
|
407
|
+
let text = ''
|
|
408
|
+
// application/x-www-form-urlencoded parser will keep the BOM.
|
|
409
|
+
// https://url.spec.whatwg.org/#concept-urlencoded-parser
|
|
410
|
+
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
|
|
411
|
+
for await (const chunk of consumeBody(this[kState].body)) {
|
|
412
|
+
if (!isUint8Array(chunk)) {
|
|
413
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
414
|
+
}
|
|
415
|
+
text += textDecoder.decode(chunk, { stream: true })
|
|
416
|
+
}
|
|
417
|
+
text += textDecoder.decode()
|
|
418
|
+
entries = new URLSearchParams(text)
|
|
344
419
|
} catch (err) {
|
|
345
420
|
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
|
|
346
421
|
// 2. If entries is failure, then throw a TypeError.
|
package/lib/fetch/constants.js
CHANGED
|
@@ -63,7 +63,7 @@ const subresource = [
|
|
|
63
63
|
/** @type {globalThis['DOMException']} */
|
|
64
64
|
const DOMException = globalThis.DOMException ?? (() => {
|
|
65
65
|
// DOMException was only made a global in Node v17.0.0,
|
|
66
|
-
// but fetch supports >= v16.
|
|
66
|
+
// but fetch supports >= v16.8.
|
|
67
67
|
try {
|
|
68
68
|
atob('~')
|
|
69
69
|
} catch (err) {
|
package/lib/fetch/dataURL.js
CHANGED
package/lib/fetch/headers.js
CHANGED
|
@@ -110,6 +110,7 @@ class HeadersList {
|
|
|
110
110
|
// https://fetch.spec.whatwg.org/#concept-header-list-set
|
|
111
111
|
set (name, value) {
|
|
112
112
|
this[kHeadersSortedMap] = null
|
|
113
|
+
name = name.toLowerCase()
|
|
113
114
|
|
|
114
115
|
// 1. If list contains name, then set the value of
|
|
115
116
|
// the first such header to value and remove the
|
package/lib/fetch/index.js
CHANGED
|
@@ -33,7 +33,8 @@ const {
|
|
|
33
33
|
isBlobLike,
|
|
34
34
|
sameOrigin,
|
|
35
35
|
isCancelled,
|
|
36
|
-
isAborted
|
|
36
|
+
isAborted,
|
|
37
|
+
isErrorLike
|
|
37
38
|
} = require('./util')
|
|
38
39
|
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
|
|
39
40
|
const assert = require('assert')
|
|
@@ -1854,7 +1855,7 @@ async function httpNetworkFetch (
|
|
|
1854
1855
|
timingInfo.decodedBodySize += bytes?.byteLength ?? 0
|
|
1855
1856
|
|
|
1856
1857
|
// 6. If bytes is failure, then terminate fetchParams’s controller.
|
|
1857
|
-
if (bytes
|
|
1858
|
+
if (isErrorLike(bytes)) {
|
|
1858
1859
|
fetchParams.controller.terminate(bytes)
|
|
1859
1860
|
return
|
|
1860
1861
|
}
|
|
@@ -1894,7 +1895,7 @@ async function httpNetworkFetch (
|
|
|
1894
1895
|
// 3. Otherwise, if stream is readable, error stream with a TypeError.
|
|
1895
1896
|
if (isReadable(stream)) {
|
|
1896
1897
|
fetchParams.controller.controller.error(new TypeError('terminated', {
|
|
1897
|
-
cause: reason
|
|
1898
|
+
cause: isErrorLike(reason) ? reason : undefined
|
|
1898
1899
|
}))
|
|
1899
1900
|
}
|
|
1900
1901
|
}
|
|
@@ -1942,14 +1943,17 @@ async function httpNetworkFetch (
|
|
|
1942
1943
|
}
|
|
1943
1944
|
|
|
1944
1945
|
let codings = []
|
|
1946
|
+
let location = ''
|
|
1945
1947
|
|
|
1946
1948
|
const headers = new Headers()
|
|
1947
1949
|
for (let n = 0; n < headersList.length; n += 2) {
|
|
1948
|
-
const key = headersList[n + 0].toString()
|
|
1949
|
-
const val = headersList[n + 1].toString()
|
|
1950
|
+
const key = headersList[n + 0].toString('latin1')
|
|
1951
|
+
const val = headersList[n + 1].toString('latin1')
|
|
1950
1952
|
|
|
1951
1953
|
if (key.toLowerCase() === 'content-encoding') {
|
|
1952
1954
|
codings = val.split(',').map((x) => x.trim())
|
|
1955
|
+
} else if (key.toLowerCase() === 'location') {
|
|
1956
|
+
location = val
|
|
1953
1957
|
}
|
|
1954
1958
|
|
|
1955
1959
|
headers.append(key, val)
|
|
@@ -1960,7 +1964,7 @@ async function httpNetworkFetch (
|
|
|
1960
1964
|
const decoders = []
|
|
1961
1965
|
|
|
1962
1966
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
|
|
1963
|
-
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) {
|
|
1967
|
+
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !(request.redirect === 'follow' && location)) {
|
|
1964
1968
|
for (const coding of codings) {
|
|
1965
1969
|
if (/(x-)?gzip/.test(coding)) {
|
|
1966
1970
|
decoders.push(zlib.createGunzip())
|
|
@@ -1980,7 +1984,7 @@ async function httpNetworkFetch (
|
|
|
1980
1984
|
statusText,
|
|
1981
1985
|
headersList: headers[kHeadersList],
|
|
1982
1986
|
body: decoders.length
|
|
1983
|
-
? pipeline(this.body, ...decoders, () => {})
|
|
1987
|
+
? pipeline(this.body, ...decoders, () => { })
|
|
1984
1988
|
: this.body.on('error', () => {})
|
|
1985
1989
|
})
|
|
1986
1990
|
|