undici 7.15.0 → 7.17.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/README.md +48 -2
- package/docs/docs/api/Agent.md +1 -0
- package/docs/docs/api/Client.md +1 -0
- package/docs/docs/api/DiagnosticsChannel.md +57 -0
- package/docs/docs/api/Dispatcher.md +86 -0
- package/docs/docs/api/Errors.md +0 -1
- package/docs/docs/api/RoundRobinPool.md +145 -0
- package/docs/docs/api/WebSocket.md +21 -0
- package/docs/docs/best-practices/crawling.md +58 -0
- package/index-fetch.js +2 -2
- package/index.js +8 -9
- package/lib/api/api-request.js +22 -8
- package/lib/api/api-upgrade.js +2 -1
- package/lib/api/readable.js +7 -5
- package/lib/core/connect.js +4 -1
- package/lib/core/diagnostics.js +28 -1
- package/lib/core/errors.js +217 -13
- package/lib/core/request.js +5 -1
- package/lib/core/symbols.js +3 -0
- package/lib/core/util.js +61 -41
- package/lib/dispatcher/agent.js +19 -7
- package/lib/dispatcher/balanced-pool.js +10 -0
- package/lib/dispatcher/client-h1.js +18 -23
- package/lib/dispatcher/client-h2.js +166 -26
- package/lib/dispatcher/client.js +64 -59
- package/lib/dispatcher/dispatcher-base.js +20 -16
- package/lib/dispatcher/env-http-proxy-agent.js +12 -16
- package/lib/dispatcher/fixed-queue.js +15 -39
- package/lib/dispatcher/h2c-client.js +7 -78
- package/lib/dispatcher/pool-base.js +60 -43
- package/lib/dispatcher/pool.js +2 -2
- package/lib/dispatcher/proxy-agent.js +27 -11
- package/lib/dispatcher/round-robin-pool.js +137 -0
- package/lib/encoding/index.js +33 -0
- package/lib/global.js +19 -1
- package/lib/handler/cache-handler.js +84 -27
- package/lib/handler/deduplication-handler.js +216 -0
- package/lib/handler/retry-handler.js +0 -2
- package/lib/interceptor/cache.js +94 -15
- package/lib/interceptor/decompress.js +2 -1
- package/lib/interceptor/deduplicate.js +109 -0
- package/lib/interceptor/dns.js +55 -13
- package/lib/mock/mock-agent.js +4 -4
- package/lib/mock/mock-errors.js +10 -0
- package/lib/mock/mock-utils.js +13 -12
- package/lib/mock/snapshot-agent.js +11 -5
- package/lib/mock/snapshot-recorder.js +12 -4
- package/lib/mock/snapshot-utils.js +4 -4
- package/lib/util/cache.js +29 -1
- package/lib/util/date.js +534 -140
- package/lib/util/runtime-features.js +124 -0
- package/lib/web/cookies/index.js +1 -1
- package/lib/web/cookies/parse.js +1 -1
- package/lib/web/eventsource/eventsource-stream.js +2 -2
- package/lib/web/eventsource/eventsource.js +34 -29
- package/lib/web/eventsource/util.js +1 -9
- package/lib/web/fetch/body.js +45 -61
- package/lib/web/fetch/data-url.js +12 -160
- package/lib/web/fetch/formdata-parser.js +204 -127
- package/lib/web/fetch/index.js +21 -19
- package/lib/web/fetch/request.js +6 -0
- package/lib/web/fetch/response.js +4 -7
- package/lib/web/fetch/util.js +10 -79
- package/lib/web/infra/index.js +229 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +6 -5
- package/lib/web/webidl/index.js +207 -44
- package/lib/web/websocket/connection.js +33 -22
- package/lib/web/websocket/events.js +1 -1
- package/lib/web/websocket/frame.js +9 -15
- package/lib/web/websocket/stream/websocketerror.js +22 -1
- package/lib/web/websocket/stream/websocketstream.js +17 -8
- package/lib/web/websocket/util.js +2 -1
- package/lib/web/websocket/websocket.js +32 -42
- package/package.json +9 -7
- package/types/agent.d.ts +2 -1
- package/types/api.d.ts +2 -2
- package/types/balanced-pool.d.ts +2 -1
- package/types/cache-interceptor.d.ts +1 -0
- package/types/client.d.ts +1 -1
- package/types/connector.d.ts +2 -2
- package/types/diagnostics-channel.d.ts +2 -2
- package/types/dispatcher.d.ts +12 -12
- package/types/errors.d.ts +5 -15
- package/types/fetch.d.ts +4 -4
- package/types/formdata.d.ts +1 -1
- package/types/h2c-client.d.ts +1 -1
- package/types/index.d.ts +9 -1
- package/types/interceptors.d.ts +36 -2
- package/types/pool.d.ts +1 -1
- package/types/readable.d.ts +2 -2
- package/types/round-robin-pool.d.ts +41 -0
- package/types/webidl.d.ts +82 -21
- package/types/websocket.d.ts +9 -9
|
@@ -18,54 +18,55 @@ const kAddClient = Symbol('add client')
|
|
|
18
18
|
const kRemoveClient = Symbol('remove client')
|
|
19
19
|
|
|
20
20
|
class PoolBase extends DispatcherBase {
|
|
21
|
-
|
|
22
|
-
super()
|
|
21
|
+
[kQueue] = new FixedQueue();
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
this[kClients] = []
|
|
26
|
-
this[kQueued] = 0
|
|
23
|
+
[kQueued] = 0;
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
[kClients] = [];
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
const queue = pool[kQueue]
|
|
27
|
+
[kNeedDrain] = false;
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
[kOnDrain] (client, origin, targets) {
|
|
30
|
+
const queue = this[kQueue]
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
needDrain = !this.dispatch(item.opts, item.handler)
|
|
32
|
+
let needDrain = false
|
|
33
|
+
|
|
34
|
+
while (!needDrain) {
|
|
35
|
+
const item = queue.shift()
|
|
36
|
+
if (!item) {
|
|
37
|
+
break
|
|
42
38
|
}
|
|
39
|
+
this[kQueued]--
|
|
40
|
+
needDrain = !client.dispatch(item.opts, item.handler)
|
|
41
|
+
}
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
client[kNeedDrain] = needDrain
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
if (!needDrain && this[kNeedDrain]) {
|
|
46
|
+
this[kNeedDrain] = false
|
|
47
|
+
this.emit('drain', origin, [this, ...targets])
|
|
48
|
+
}
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
if (this[kClosedResolve] && queue.isEmpty()) {
|
|
51
|
+
const closeAll = new Array(this[kClients].length)
|
|
52
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
53
|
+
closeAll[i] = this[kClients][i].close()
|
|
55
54
|
}
|
|
55
|
+
return Promise.all(closeAll)
|
|
56
|
+
.then(this[kClosedResolve])
|
|
56
57
|
}
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
[kOnConnect] = (origin, targets) => {
|
|
61
|
+
this.emit('connect', origin, [this, ...targets])
|
|
62
|
+
};
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
[kOnDisconnect] = (origin, targets, err) => {
|
|
65
|
+
this.emit('disconnect', origin, [this, ...targets], err)
|
|
66
|
+
};
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
68
|
+
[kOnConnectionError] = (origin, targets, err) => {
|
|
69
|
+
this.emit('connectionError', origin, [this, ...targets], err)
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
get [kBusy] () {
|
|
@@ -73,11 +74,19 @@ class PoolBase extends DispatcherBase {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
get [kConnected] () {
|
|
76
|
-
|
|
77
|
+
let ret = 0
|
|
78
|
+
for (const { [kConnected]: connected } of this[kClients]) {
|
|
79
|
+
ret += connected
|
|
80
|
+
}
|
|
81
|
+
return ret
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
get [kFree] () {
|
|
80
|
-
|
|
85
|
+
let ret = 0
|
|
86
|
+
for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) {
|
|
87
|
+
ret += connected && !needDrain
|
|
88
|
+
}
|
|
89
|
+
return ret
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
get [kPending] () {
|
|
@@ -108,17 +117,21 @@ class PoolBase extends DispatcherBase {
|
|
|
108
117
|
return new PoolStats(this)
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
[kClose] () {
|
|
112
121
|
if (this[kQueue].isEmpty()) {
|
|
113
|
-
|
|
122
|
+
const closeAll = new Array(this[kClients].length)
|
|
123
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
124
|
+
closeAll[i] = this[kClients][i].close()
|
|
125
|
+
}
|
|
126
|
+
return Promise.all(closeAll)
|
|
114
127
|
} else {
|
|
115
|
-
|
|
128
|
+
return new Promise((resolve) => {
|
|
116
129
|
this[kClosedResolve] = resolve
|
|
117
130
|
})
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
[kDestroy] (err) {
|
|
122
135
|
while (true) {
|
|
123
136
|
const item = this[kQueue].shift()
|
|
124
137
|
if (!item) {
|
|
@@ -127,7 +140,11 @@ class PoolBase extends DispatcherBase {
|
|
|
127
140
|
item.handler.onError(err)
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
|
|
143
|
+
const destroyAll = new Array(this[kClients].length)
|
|
144
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
145
|
+
destroyAll[i] = this[kClients][i].destroy(err)
|
|
146
|
+
}
|
|
147
|
+
return Promise.all(destroyAll)
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
[kDispatch] (opts, handler) {
|
|
@@ -147,7 +164,7 @@ class PoolBase extends DispatcherBase {
|
|
|
147
164
|
|
|
148
165
|
[kAddClient] (client) {
|
|
149
166
|
client
|
|
150
|
-
.on('drain', this[kOnDrain])
|
|
167
|
+
.on('drain', this[kOnDrain].bind(this, client))
|
|
151
168
|
.on('connect', this[kOnConnect])
|
|
152
169
|
.on('disconnect', this[kOnDisconnect])
|
|
153
170
|
.on('connectionError', this[kOnConnectionError])
|
|
@@ -157,7 +174,7 @@ class PoolBase extends DispatcherBase {
|
|
|
157
174
|
if (this[kNeedDrain]) {
|
|
158
175
|
queueMicrotask(() => {
|
|
159
176
|
if (this[kNeedDrain]) {
|
|
160
|
-
this[kOnDrain](client[kUrl], [
|
|
177
|
+
this[kOnDrain](client, client[kUrl], [client, this])
|
|
161
178
|
}
|
|
162
179
|
})
|
|
163
180
|
}
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -51,8 +51,6 @@ class Pool extends PoolBase {
|
|
|
51
51
|
throw new InvalidArgumentError('connect must be a function or an object')
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
super()
|
|
55
|
-
|
|
56
54
|
if (typeof connect !== 'function') {
|
|
57
55
|
connect = buildConnector({
|
|
58
56
|
...tls,
|
|
@@ -65,6 +63,8 @@ class Pool extends PoolBase {
|
|
|
65
63
|
})
|
|
66
64
|
}
|
|
67
65
|
|
|
66
|
+
super()
|
|
67
|
+
|
|
68
68
|
this[kConnections] = connections || null
|
|
69
69
|
this[kUrl] = util.parseOrigin(origin)
|
|
70
70
|
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
|
|
@@ -7,6 +7,7 @@ const DispatcherBase = require('./dispatcher-base')
|
|
|
7
7
|
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
|
|
8
8
|
const buildConnector = require('../core/connect')
|
|
9
9
|
const Client = require('./client')
|
|
10
|
+
const { channels } = require('../core/diagnostics')
|
|
10
11
|
|
|
11
12
|
const kAgent = Symbol('proxy agent')
|
|
12
13
|
const kClient = Symbol('proxy client')
|
|
@@ -37,11 +38,12 @@ class Http1ProxyWrapper extends DispatcherBase {
|
|
|
37
38
|
#client
|
|
38
39
|
|
|
39
40
|
constructor (proxyUrl, { headers = {}, connect, factory }) {
|
|
40
|
-
super()
|
|
41
41
|
if (!proxyUrl) {
|
|
42
42
|
throw new InvalidArgumentError('Proxy URL is mandatory')
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
super()
|
|
46
|
+
|
|
45
47
|
this[kProxyHeaders] = headers
|
|
46
48
|
if (factory) {
|
|
47
49
|
this.#client = factory(proxyUrl, { connect })
|
|
@@ -80,11 +82,11 @@ class Http1ProxyWrapper extends DispatcherBase {
|
|
|
80
82
|
return this.#client[kDispatch](opts, handler)
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
[kClose] () {
|
|
84
86
|
return this.#client.close()
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
[kDestroy] (err) {
|
|
88
90
|
return this.#client.destroy(err)
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -149,7 +151,7 @@ class ProxyAgent extends DispatcherBase {
|
|
|
149
151
|
requestedPath += `:${defaultProtocolPort(opts.protocol)}`
|
|
150
152
|
}
|
|
151
153
|
try {
|
|
152
|
-
const
|
|
154
|
+
const connectParams = {
|
|
153
155
|
origin,
|
|
154
156
|
port,
|
|
155
157
|
path: requestedPath,
|
|
@@ -160,11 +162,21 @@ class ProxyAgent extends DispatcherBase {
|
|
|
160
162
|
...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {})
|
|
161
163
|
},
|
|
162
164
|
servername: this[kProxyTls]?.servername || proxyHostname
|
|
163
|
-
}
|
|
165
|
+
}
|
|
166
|
+
const { socket, statusCode } = await this[kClient].connect(connectParams)
|
|
164
167
|
if (statusCode !== 200) {
|
|
165
168
|
socket.on('error', noop).destroy()
|
|
166
169
|
callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (channels.proxyConnected.hasSubscribers) {
|
|
174
|
+
channels.proxyConnected.publish({
|
|
175
|
+
socket,
|
|
176
|
+
connectParams
|
|
177
|
+
})
|
|
167
178
|
}
|
|
179
|
+
|
|
168
180
|
if (opts.protocol !== 'https:') {
|
|
169
181
|
callback(null, socket)
|
|
170
182
|
return
|
|
@@ -220,14 +232,18 @@ class ProxyAgent extends DispatcherBase {
|
|
|
220
232
|
}
|
|
221
233
|
}
|
|
222
234
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
[kClose] () {
|
|
236
|
+
return Promise.all([
|
|
237
|
+
this[kAgent].close(),
|
|
238
|
+
this[kClient].close()
|
|
239
|
+
])
|
|
226
240
|
}
|
|
227
241
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
242
|
+
[kDestroy] () {
|
|
243
|
+
return Promise.all([
|
|
244
|
+
this[kAgent].destroy(),
|
|
245
|
+
this[kClient].destroy()
|
|
246
|
+
])
|
|
231
247
|
}
|
|
232
248
|
}
|
|
233
249
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
PoolBase,
|
|
5
|
+
kClients,
|
|
6
|
+
kNeedDrain,
|
|
7
|
+
kAddClient,
|
|
8
|
+
kGetDispatcher,
|
|
9
|
+
kRemoveClient
|
|
10
|
+
} = require('./pool-base')
|
|
11
|
+
const Client = require('./client')
|
|
12
|
+
const {
|
|
13
|
+
InvalidArgumentError
|
|
14
|
+
} = require('../core/errors')
|
|
15
|
+
const util = require('../core/util')
|
|
16
|
+
const { kUrl } = require('../core/symbols')
|
|
17
|
+
const buildConnector = require('../core/connect')
|
|
18
|
+
|
|
19
|
+
const kOptions = Symbol('options')
|
|
20
|
+
const kConnections = Symbol('connections')
|
|
21
|
+
const kFactory = Symbol('factory')
|
|
22
|
+
const kIndex = Symbol('index')
|
|
23
|
+
|
|
24
|
+
function defaultFactory (origin, opts) {
|
|
25
|
+
return new Client(origin, opts)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class RoundRobinPool extends PoolBase {
|
|
29
|
+
constructor (origin, {
|
|
30
|
+
connections,
|
|
31
|
+
factory = defaultFactory,
|
|
32
|
+
connect,
|
|
33
|
+
connectTimeout,
|
|
34
|
+
tls,
|
|
35
|
+
maxCachedSessions,
|
|
36
|
+
socketPath,
|
|
37
|
+
autoSelectFamily,
|
|
38
|
+
autoSelectFamilyAttemptTimeout,
|
|
39
|
+
allowH2,
|
|
40
|
+
clientTtl,
|
|
41
|
+
...options
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
|
|
44
|
+
throw new InvalidArgumentError('invalid connections')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof factory !== 'function') {
|
|
48
|
+
throw new InvalidArgumentError('factory must be a function.')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
|
|
52
|
+
throw new InvalidArgumentError('connect must be a function or an object')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof connect !== 'function') {
|
|
56
|
+
connect = buildConnector({
|
|
57
|
+
...tls,
|
|
58
|
+
maxCachedSessions,
|
|
59
|
+
allowH2,
|
|
60
|
+
socketPath,
|
|
61
|
+
timeout: connectTimeout,
|
|
62
|
+
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
|
|
63
|
+
...connect
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
super()
|
|
68
|
+
|
|
69
|
+
this[kConnections] = connections || null
|
|
70
|
+
this[kUrl] = util.parseOrigin(origin)
|
|
71
|
+
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
|
|
72
|
+
this[kOptions].interceptors = options.interceptors
|
|
73
|
+
? { ...options.interceptors }
|
|
74
|
+
: undefined
|
|
75
|
+
this[kFactory] = factory
|
|
76
|
+
this[kIndex] = -1
|
|
77
|
+
|
|
78
|
+
this.on('connect', (origin, targets) => {
|
|
79
|
+
if (clientTtl != null && clientTtl > 0) {
|
|
80
|
+
for (const target of targets) {
|
|
81
|
+
Object.assign(target, { ttl: Date.now() })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
this.on('connectionError', (origin, targets, error) => {
|
|
87
|
+
for (const target of targets) {
|
|
88
|
+
const idx = this[kClients].indexOf(target)
|
|
89
|
+
if (idx !== -1) {
|
|
90
|
+
this[kClients].splice(idx, 1)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[kGetDispatcher] () {
|
|
97
|
+
const clientTtlOption = this[kOptions].clientTtl
|
|
98
|
+
const clientsLength = this[kClients].length
|
|
99
|
+
|
|
100
|
+
// If we have no clients yet, create one
|
|
101
|
+
if (clientsLength === 0) {
|
|
102
|
+
const dispatcher = this[kFactory](this[kUrl], this[kOptions])
|
|
103
|
+
this[kAddClient](dispatcher)
|
|
104
|
+
return dispatcher
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Round-robin through existing clients
|
|
108
|
+
let checked = 0
|
|
109
|
+
while (checked < clientsLength) {
|
|
110
|
+
this[kIndex] = (this[kIndex] + 1) % clientsLength
|
|
111
|
+
const client = this[kClients][this[kIndex]]
|
|
112
|
+
|
|
113
|
+
// Check if client is stale (TTL expired)
|
|
114
|
+
if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
|
|
115
|
+
this[kRemoveClient](client)
|
|
116
|
+
checked++
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Return client if it's not draining
|
|
121
|
+
if (!client[kNeedDrain]) {
|
|
122
|
+
return client
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
checked++
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// All clients are busy, create a new one if we haven't reached the limit
|
|
129
|
+
if (!this[kConnections] || clientsLength < this[kConnections]) {
|
|
130
|
+
const dispatcher = this[kFactory](this[kUrl], this[kOptions])
|
|
131
|
+
this[kAddClient](dispatcher)
|
|
132
|
+
return dispatcher
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = RoundRobinPool
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const textDecoder = new TextDecoder()
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @see https://encoding.spec.whatwg.org/#utf-8-decode
|
|
7
|
+
* @param {Uint8Array} buffer
|
|
8
|
+
*/
|
|
9
|
+
function utf8DecodeBytes (buffer) {
|
|
10
|
+
if (buffer.length === 0) {
|
|
11
|
+
return ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 1. Let buffer be the result of peeking three bytes from
|
|
15
|
+
// ioQueue, converted to a byte sequence.
|
|
16
|
+
|
|
17
|
+
// 2. If buffer is 0xEF 0xBB 0xBF, then read three
|
|
18
|
+
// bytes from ioQueue. (Do nothing with those bytes.)
|
|
19
|
+
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
|
|
20
|
+
buffer = buffer.subarray(3)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 3. Process a queue with an instance of UTF-8’s
|
|
24
|
+
// decoder, ioQueue, output, and "replacement".
|
|
25
|
+
const output = textDecoder.decode(buffer)
|
|
26
|
+
|
|
27
|
+
// 4. Return output.
|
|
28
|
+
return output
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
utf8DecodeBytes
|
|
33
|
+
}
|
package/lib/global.js
CHANGED
|
@@ -26,7 +26,25 @@ function getGlobalDispatcher () {
|
|
|
26
26
|
return globalThis[globalDispatcher]
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// These are the globals that can be installed by undici.install().
|
|
30
|
+
// Not exported by index.js to avoid use outside of this module.
|
|
31
|
+
const installedExports = /** @type {const} */ (
|
|
32
|
+
[
|
|
33
|
+
'fetch',
|
|
34
|
+
'Headers',
|
|
35
|
+
'Response',
|
|
36
|
+
'Request',
|
|
37
|
+
'FormData',
|
|
38
|
+
'WebSocket',
|
|
39
|
+
'CloseEvent',
|
|
40
|
+
'ErrorEvent',
|
|
41
|
+
'MessageEvent',
|
|
42
|
+
'EventSource'
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
29
46
|
module.exports = {
|
|
30
47
|
setGlobalDispatcher,
|
|
31
|
-
getGlobalDispatcher
|
|
48
|
+
getGlobalDispatcher,
|
|
49
|
+
installedExports
|
|
32
50
|
}
|
|
@@ -17,11 +17,11 @@ const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
|
|
|
17
17
|
|
|
18
18
|
// Status codes which semantic is not handled by the cache
|
|
19
19
|
// https://datatracker.ietf.org/doc/html/rfc9111#section-3
|
|
20
|
-
// This list should not grow beyond 206
|
|
20
|
+
// This list should not grow beyond 206 unless the RFC is updated
|
|
21
21
|
// by a newer one including more. Please introduce another list if
|
|
22
22
|
// implementing caching of responses with the 'must-understand' directive.
|
|
23
23
|
const NOT_UNDERSTOOD_STATUS_CODES = [
|
|
24
|
-
206
|
|
24
|
+
206
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
const MAX_RESPONSE_AGE = 2147483647000
|
|
@@ -104,6 +104,7 @@ class CacheHandler {
|
|
|
104
104
|
resHeaders,
|
|
105
105
|
statusMessage
|
|
106
106
|
)
|
|
107
|
+
const handler = this
|
|
107
108
|
|
|
108
109
|
if (
|
|
109
110
|
!util.safeHTTPMethods.includes(this.#cacheKey.method) &&
|
|
@@ -189,36 +190,92 @@ class CacheHandler {
|
|
|
189
190
|
deleteAt
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
// Not modified, re-use the cached value
|
|
194
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
|
|
195
|
+
if (statusCode === 304) {
|
|
196
|
+
/**
|
|
197
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
|
|
198
|
+
*/
|
|
199
|
+
const cachedValue = this.#store.get(this.#cacheKey)
|
|
200
|
+
if (!cachedValue) {
|
|
201
|
+
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
|
|
202
|
+
return downstreamOnHeaders()
|
|
203
|
+
}
|
|
195
204
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
// Re-use the cached value: statuscode, statusmessage, headers and body
|
|
206
|
+
value.statusCode = cachedValue.statusCode
|
|
207
|
+
value.statusMessage = cachedValue.statusMessage
|
|
208
|
+
value.etag = cachedValue.etag
|
|
209
|
+
value.headers = { ...cachedValue.headers, ...strippedHeaders }
|
|
200
210
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
handler
|
|
211
|
+
downstreamOnHeaders()
|
|
212
|
+
|
|
213
|
+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
|
|
214
|
+
|
|
215
|
+
if (!this.#writeStream || !cachedValue?.body) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const bodyIterator = cachedValue.body.values()
|
|
220
|
+
|
|
221
|
+
const streamCachedBody = () => {
|
|
222
|
+
for (const chunk of bodyIterator) {
|
|
223
|
+
const full = this.#writeStream.write(chunk) === false
|
|
224
|
+
this.#handler.onResponseData?.(controller, chunk)
|
|
225
|
+
// when stream is full stop writing until we get a 'drain' event
|
|
226
|
+
if (full) {
|
|
227
|
+
break
|
|
228
|
+
}
|
|
215
229
|
}
|
|
230
|
+
}
|
|
216
231
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
this.#writeStream
|
|
233
|
+
.on('error', function () {
|
|
234
|
+
handler.#writeStream = undefined
|
|
235
|
+
handler.#store.delete(handler.#cacheKey)
|
|
236
|
+
})
|
|
237
|
+
.on('drain', () => {
|
|
238
|
+
streamCachedBody()
|
|
239
|
+
})
|
|
240
|
+
.on('close', function () {
|
|
241
|
+
if (handler.#writeStream === this) {
|
|
242
|
+
handler.#writeStream = undefined
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
streamCachedBody()
|
|
247
|
+
} else {
|
|
248
|
+
if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
|
|
249
|
+
value.etag = resHeaders.etag
|
|
250
|
+
}
|
|
220
251
|
|
|
221
|
-
|
|
252
|
+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
|
|
253
|
+
|
|
254
|
+
if (!this.#writeStream) {
|
|
255
|
+
return downstreamOnHeaders()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.#writeStream
|
|
259
|
+
.on('drain', () => controller.resume())
|
|
260
|
+
.on('error', function () {
|
|
261
|
+
// TODO (fix): Make error somehow observable?
|
|
262
|
+
handler.#writeStream = undefined
|
|
263
|
+
|
|
264
|
+
// Delete the value in case the cache store is holding onto state from
|
|
265
|
+
// the call to createWriteStream
|
|
266
|
+
handler.#store.delete(handler.#cacheKey)
|
|
267
|
+
})
|
|
268
|
+
.on('close', function () {
|
|
269
|
+
if (handler.#writeStream === this) {
|
|
270
|
+
handler.#writeStream = undefined
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// TODO (fix): Should we resume even if was paused downstream?
|
|
274
|
+
controller.resume()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
downstreamOnHeaders()
|
|
278
|
+
}
|
|
222
279
|
}
|
|
223
280
|
|
|
224
281
|
onResponseData (controller, chunk) {
|