undici 7.0.0-alpha.2 → 7.0.0-alpha.4
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 +3 -2
- package/docs/docs/api/BalancedPool.md +1 -1
- package/docs/docs/api/CacheStore.md +100 -0
- package/docs/docs/api/Dispatcher.md +32 -2
- package/docs/docs/api/MockClient.md +1 -1
- package/docs/docs/api/Pool.md +1 -1
- package/docs/docs/api/api-lifecycle.md +2 -2
- package/docs/docs/best-practices/mocking-request.md +2 -2
- package/docs/docs/best-practices/proxy.md +1 -1
- package/index.d.ts +1 -1
- package/index.js +8 -2
- package/lib/api/api-request.js +2 -2
- package/lib/api/readable.js +6 -6
- package/lib/cache/memory-cache-store.js +325 -0
- package/lib/core/connect.js +5 -0
- package/lib/core/constants.js +24 -1
- package/lib/core/request.js +2 -2
- package/lib/core/util.js +13 -1
- package/lib/dispatcher/client-h1.js +100 -87
- package/lib/dispatcher/client-h2.js +168 -96
- package/lib/dispatcher/pool-base.js +3 -3
- package/lib/handler/cache-handler.js +389 -0
- package/lib/handler/cache-revalidation-handler.js +151 -0
- package/lib/handler/redirect-handler.js +5 -3
- package/lib/handler/retry-handler.js +3 -3
- package/lib/interceptor/cache.js +192 -0
- package/lib/interceptor/dns.js +71 -48
- package/lib/util/cache.js +249 -0
- package/lib/web/cache/cache.js +1 -0
- package/lib/web/cache/cachestorage.js +2 -0
- package/lib/web/cookies/index.js +12 -1
- package/lib/web/cookies/parse.js +6 -1
- package/lib/web/eventsource/eventsource.js +2 -0
- package/lib/web/fetch/body.js +1 -5
- package/lib/web/fetch/constants.js +12 -5
- package/lib/web/fetch/data-url.js +2 -2
- package/lib/web/fetch/formdata-parser.js +70 -43
- package/lib/web/fetch/formdata.js +3 -1
- package/lib/web/fetch/headers.js +3 -1
- package/lib/web/fetch/index.js +4 -6
- package/lib/web/fetch/request.js +3 -1
- package/lib/web/fetch/response.js +3 -1
- package/lib/web/fetch/util.js +171 -47
- package/lib/web/fetch/webidl.js +28 -16
- package/lib/web/websocket/constants.js +67 -6
- package/lib/web/websocket/events.js +4 -0
- package/lib/web/websocket/stream/websocketerror.js +1 -1
- package/lib/web/websocket/websocket.js +2 -0
- package/package.json +8 -5
- package/types/cache-interceptor.d.ts +101 -0
- package/types/cookies.d.ts +2 -0
- package/types/dispatcher.d.ts +1 -1
- package/types/fetch.d.ts +9 -8
- package/types/index.d.ts +3 -1
- package/types/interceptors.d.ts +4 -1
- package/types/webidl.d.ts +7 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const util = require('../core/util')
|
|
4
|
+
const DecoratorHandler = require('../handler/decorator-handler')
|
|
5
|
+
const {
|
|
6
|
+
parseCacheControlHeader,
|
|
7
|
+
parseVaryHeader
|
|
8
|
+
} = require('../util/cache')
|
|
9
|
+
|
|
10
|
+
function noop () {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Writes a response to a CacheStore and then passes it on to the next handler
|
|
14
|
+
*/
|
|
15
|
+
class CacheHandler extends DecoratorHandler {
|
|
16
|
+
/**
|
|
17
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
18
|
+
*/
|
|
19
|
+
#cacheKey
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
|
|
23
|
+
*/
|
|
24
|
+
#store
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
|
|
28
|
+
*/
|
|
29
|
+
#handler
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @type {import('node:stream').Writable | undefined}
|
|
33
|
+
*/
|
|
34
|
+
#writeStream
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
|
|
38
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
39
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
40
|
+
*/
|
|
41
|
+
constructor (opts, cacheKey, handler) {
|
|
42
|
+
const { store } = opts
|
|
43
|
+
|
|
44
|
+
super(handler)
|
|
45
|
+
|
|
46
|
+
this.#store = store
|
|
47
|
+
this.#cacheKey = cacheKey
|
|
48
|
+
this.#handler = handler
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onConnect (abort) {
|
|
52
|
+
if (this.#writeStream) {
|
|
53
|
+
this.#writeStream.destroy()
|
|
54
|
+
this.#writeStream = undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof this.#handler.onConnect === 'function') {
|
|
58
|
+
this.#handler.onConnect(abort)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @see {DispatchHandlers.onHeaders}
|
|
64
|
+
*
|
|
65
|
+
* @param {number} statusCode
|
|
66
|
+
* @param {Buffer[]} rawHeaders
|
|
67
|
+
* @param {() => void} resume
|
|
68
|
+
* @param {string} statusMessage
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
onHeaders (
|
|
72
|
+
statusCode,
|
|
73
|
+
rawHeaders,
|
|
74
|
+
resume,
|
|
75
|
+
statusMessage
|
|
76
|
+
) {
|
|
77
|
+
const downstreamOnHeaders = () => {
|
|
78
|
+
if (typeof this.#handler.onHeaders === 'function') {
|
|
79
|
+
return this.#handler.onHeaders(
|
|
80
|
+
statusCode,
|
|
81
|
+
rawHeaders,
|
|
82
|
+
resume,
|
|
83
|
+
statusMessage
|
|
84
|
+
)
|
|
85
|
+
} else {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
!util.safeHTTPMethods.includes(this.#cacheKey.method) &&
|
|
92
|
+
statusCode >= 200 &&
|
|
93
|
+
statusCode <= 399
|
|
94
|
+
) {
|
|
95
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
|
|
96
|
+
try {
|
|
97
|
+
this.#store.delete(this.#cacheKey).catch?.(noop)
|
|
98
|
+
} catch {
|
|
99
|
+
// Fail silently
|
|
100
|
+
}
|
|
101
|
+
return downstreamOnHeaders()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parsedRawHeaders = util.parseRawHeaders(rawHeaders)
|
|
105
|
+
const headers = util.parseHeaders(parsedRawHeaders)
|
|
106
|
+
|
|
107
|
+
const cacheControlHeader = headers['cache-control']
|
|
108
|
+
const isCacheFull = typeof this.#store.isFull !== 'undefined'
|
|
109
|
+
? this.#store.isFull
|
|
110
|
+
: false
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
!cacheControlHeader ||
|
|
114
|
+
isCacheFull
|
|
115
|
+
) {
|
|
116
|
+
// Don't have the cache control header or the cache is full
|
|
117
|
+
return downstreamOnHeaders()
|
|
118
|
+
}
|
|
119
|
+
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
|
|
120
|
+
if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
|
|
121
|
+
return downstreamOnHeaders()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const now = Date.now()
|
|
125
|
+
const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
|
|
126
|
+
if (staleAt) {
|
|
127
|
+
const varyDirectives = this.#cacheKey.headers && headers.vary
|
|
128
|
+
? parseVaryHeader(headers.vary, this.#cacheKey.headers)
|
|
129
|
+
: undefined
|
|
130
|
+
const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
|
|
131
|
+
|
|
132
|
+
const strippedHeaders = stripNecessaryHeaders(
|
|
133
|
+
rawHeaders,
|
|
134
|
+
parsedRawHeaders,
|
|
135
|
+
cacheControlDirectives
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, {
|
|
139
|
+
statusCode,
|
|
140
|
+
statusMessage,
|
|
141
|
+
rawHeaders: strippedHeaders,
|
|
142
|
+
vary: varyDirectives,
|
|
143
|
+
cachedAt: now,
|
|
144
|
+
staleAt,
|
|
145
|
+
deleteAt
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (this.#writeStream) {
|
|
149
|
+
const handler = this
|
|
150
|
+
this.#writeStream
|
|
151
|
+
.on('drain', resume)
|
|
152
|
+
.on('error', function () {
|
|
153
|
+
// TODO (fix): Make error somehow observable?
|
|
154
|
+
})
|
|
155
|
+
.on('close', function () {
|
|
156
|
+
if (handler.#writeStream === this) {
|
|
157
|
+
handler.#writeStream = undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// TODO (fix): Should we resume even if was paused downstream?
|
|
161
|
+
resume()
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return downstreamOnHeaders()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @see {DispatchHandlers.onData}
|
|
171
|
+
*
|
|
172
|
+
* @param {Buffer} chunk
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
onData (chunk) {
|
|
176
|
+
let paused = false
|
|
177
|
+
|
|
178
|
+
if (this.#writeStream) {
|
|
179
|
+
paused ||= this.#writeStream.write(chunk) === false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof this.#handler.onData === 'function') {
|
|
183
|
+
paused ||= this.#handler.onData(chunk) === false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return !paused
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @see {DispatchHandlers.onComplete}
|
|
191
|
+
*
|
|
192
|
+
* @param {string[] | null} rawTrailers
|
|
193
|
+
*/
|
|
194
|
+
onComplete (rawTrailers) {
|
|
195
|
+
if (this.#writeStream) {
|
|
196
|
+
this.#writeStream.end()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (typeof this.#handler.onComplete === 'function') {
|
|
200
|
+
return this.#handler.onComplete(rawTrailers)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @see {DispatchHandlers.onError}
|
|
206
|
+
*
|
|
207
|
+
* @param {Error} err
|
|
208
|
+
*/
|
|
209
|
+
onError (err) {
|
|
210
|
+
if (this.#writeStream) {
|
|
211
|
+
this.#writeStream.destroy(err)
|
|
212
|
+
this.#writeStream = undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (typeof this.#handler.onError === 'function') {
|
|
216
|
+
this.#handler.onError(err)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
223
|
+
*
|
|
224
|
+
* @param {number} statusCode
|
|
225
|
+
* @param {Record<string, string | string[]>} headers
|
|
226
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
227
|
+
*/
|
|
228
|
+
function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
229
|
+
if (
|
|
230
|
+
statusCode !== 200 &&
|
|
231
|
+
statusCode !== 307
|
|
232
|
+
) {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
cacheControlDirectives.private === true ||
|
|
238
|
+
cacheControlDirectives['no-cache'] === true ||
|
|
239
|
+
cacheControlDirectives['no-store']
|
|
240
|
+
) {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
|
|
245
|
+
if (headers.vary === '*') {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
250
|
+
if (headers.authorization) {
|
|
251
|
+
if (!cacheControlDirectives.public || typeof headers.authorization !== 'string') {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
Array.isArray(cacheControlDirectives['no-cache']) &&
|
|
257
|
+
cacheControlDirectives['no-cache'].includes('authorization')
|
|
258
|
+
) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
Array.isArray(cacheControlDirectives['private']) &&
|
|
264
|
+
cacheControlDirectives['private'].includes('authorization')
|
|
265
|
+
) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @param {number} now
|
|
275
|
+
* @param {Record<string, string | string[]>} headers
|
|
276
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
277
|
+
*
|
|
278
|
+
* @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
|
|
279
|
+
*/
|
|
280
|
+
function determineStaleAt (now, headers, cacheControlDirectives) {
|
|
281
|
+
// Prioritize s-maxage since we're a shared cache
|
|
282
|
+
// s-maxage > max-age > Expire
|
|
283
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
|
|
284
|
+
const sMaxAge = cacheControlDirectives['s-maxage']
|
|
285
|
+
if (sMaxAge) {
|
|
286
|
+
return now + (sMaxAge * 1000)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (cacheControlDirectives.immutable) {
|
|
290
|
+
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
|
|
291
|
+
return now + 31536000
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const maxAge = cacheControlDirectives['max-age']
|
|
295
|
+
if (maxAge) {
|
|
296
|
+
return now + (maxAge * 1000)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (headers.expire && typeof headers.expire === 'string') {
|
|
300
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
|
|
301
|
+
const expiresDate = new Date(headers.expire)
|
|
302
|
+
if (expiresDate instanceof Date && !isNaN(expiresDate)) {
|
|
303
|
+
return now + (Date.now() - expiresDate.getTime())
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return undefined
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @param {number} now
|
|
312
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
313
|
+
* @param {number} staleAt
|
|
314
|
+
*/
|
|
315
|
+
function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
316
|
+
if (cacheControlDirectives['stale-while-revalidate']) {
|
|
317
|
+
return now + (cacheControlDirectives['stale-while-revalidate'] * 1000)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return staleAt
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Strips headers required to be removed in cached responses
|
|
325
|
+
* @param {Buffer[]} rawHeaders
|
|
326
|
+
* @param {string[]} parsedRawHeaders
|
|
327
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
328
|
+
* @returns {Buffer[]}
|
|
329
|
+
*/
|
|
330
|
+
function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirectives) {
|
|
331
|
+
const headersToRemove = ['connection']
|
|
332
|
+
|
|
333
|
+
if (Array.isArray(cacheControlDirectives['no-cache'])) {
|
|
334
|
+
headersToRemove.push(...cacheControlDirectives['no-cache'])
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (Array.isArray(cacheControlDirectives['private'])) {
|
|
338
|
+
headersToRemove.push(...cacheControlDirectives['private'])
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let strippedHeaders
|
|
342
|
+
|
|
343
|
+
let offset = 0
|
|
344
|
+
for (let i = 0; i < parsedRawHeaders.length; i += 2) {
|
|
345
|
+
const headerName = parsedRawHeaders[i]
|
|
346
|
+
|
|
347
|
+
if (headersToRemove.includes(headerName)) {
|
|
348
|
+
// We have at least one header we want to remove
|
|
349
|
+
if (!strippedHeaders) {
|
|
350
|
+
// This is the first header we want to remove, let's create the array
|
|
351
|
+
// Since we're stripping headers, this will over allocate. We'll trim
|
|
352
|
+
// it later.
|
|
353
|
+
strippedHeaders = new Array(parsedRawHeaders.length)
|
|
354
|
+
|
|
355
|
+
// Backfill the previous headers into it
|
|
356
|
+
for (let j = 0; j < i; j += 2) {
|
|
357
|
+
strippedHeaders[j] = parsedRawHeaders[j]
|
|
358
|
+
strippedHeaders[j + 1] = parsedRawHeaders[j + 1]
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// We can't map indices 1:1 from stripped headers to rawHeaders without
|
|
363
|
+
// creating holes (if we skip a header, we now have two holes where at
|
|
364
|
+
// element should be). So, let's keep an offset to keep strippedHeaders
|
|
365
|
+
// flattened. We can also use this at the end for trimming the empty
|
|
366
|
+
// elements off of strippedHeaders.
|
|
367
|
+
offset += 2
|
|
368
|
+
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// We want to keep this header. Let's add it to strippedHeaders if it exists
|
|
373
|
+
if (strippedHeaders) {
|
|
374
|
+
strippedHeaders[i - offset] = parsedRawHeaders[i]
|
|
375
|
+
strippedHeaders[i + 1 - offset] = parsedRawHeaders[i + 1]
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (strippedHeaders) {
|
|
380
|
+
// Trim off the empty values at the end
|
|
381
|
+
strippedHeaders.length -= offset
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return strippedHeaders
|
|
385
|
+
? util.encodeRawHeaders(strippedHeaders)
|
|
386
|
+
: rawHeaders
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = CacheHandler
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const DecoratorHandler = require('../handler/decorator-handler')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This takes care of revalidation requests we send to the origin. If we get
|
|
8
|
+
* a response indicating that what we have is cached (via a HTTP 304), we can
|
|
9
|
+
* continue using the cached value. Otherwise, we'll receive the new response
|
|
10
|
+
* here, which we then just pass on to the next handler (most likely a
|
|
11
|
+
* CacheHandler). Note that this assumes the proper headers were already
|
|
12
|
+
* included in the request to tell the origin that we want to revalidate the
|
|
13
|
+
* response (i.e. if-modified-since).
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
|
|
16
|
+
*
|
|
17
|
+
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers
|
|
18
|
+
* @implements {DispatchHandlers}
|
|
19
|
+
*/
|
|
20
|
+
class CacheRevalidationHandler extends DecoratorHandler {
|
|
21
|
+
#successful = false
|
|
22
|
+
/**
|
|
23
|
+
* @type {((boolean) => void) | null}
|
|
24
|
+
*/
|
|
25
|
+
#callback
|
|
26
|
+
/**
|
|
27
|
+
* @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)}
|
|
28
|
+
*/
|
|
29
|
+
#handler
|
|
30
|
+
|
|
31
|
+
#abort
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {(boolean) => void} callback Function to call if the cached value is valid
|
|
35
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
36
|
+
*/
|
|
37
|
+
constructor (callback, handler) {
|
|
38
|
+
if (typeof callback !== 'function') {
|
|
39
|
+
throw new TypeError('callback must be a function')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
super(handler)
|
|
43
|
+
|
|
44
|
+
this.#callback = callback
|
|
45
|
+
this.#handler = handler
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onConnect (abort) {
|
|
49
|
+
this.#successful = false
|
|
50
|
+
this.#abort = abort
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @see {DispatchHandlers.onHeaders}
|
|
55
|
+
*
|
|
56
|
+
* @param {number} statusCode
|
|
57
|
+
* @param {Buffer[]} rawHeaders
|
|
58
|
+
* @param {() => void} resume
|
|
59
|
+
* @param {string} statusMessage
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
onHeaders (
|
|
63
|
+
statusCode,
|
|
64
|
+
rawHeaders,
|
|
65
|
+
resume,
|
|
66
|
+
statusMessage
|
|
67
|
+
) {
|
|
68
|
+
assert(this.#callback != null)
|
|
69
|
+
|
|
70
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
|
|
71
|
+
this.#successful = statusCode === 304
|
|
72
|
+
this.#callback(this.#successful)
|
|
73
|
+
this.#callback = null
|
|
74
|
+
|
|
75
|
+
if (this.#successful) {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof this.#handler.onConnect === 'function') {
|
|
80
|
+
this.#handler.onConnect(this.#abort)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof this.#handler.onHeaders === 'function') {
|
|
84
|
+
return this.#handler.onHeaders(
|
|
85
|
+
statusCode,
|
|
86
|
+
rawHeaders,
|
|
87
|
+
resume,
|
|
88
|
+
statusMessage
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @see {DispatchHandlers.onData}
|
|
97
|
+
*
|
|
98
|
+
* @param {Buffer} chunk
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
onData (chunk) {
|
|
102
|
+
if (this.#successful) {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof this.#handler.onData === 'function') {
|
|
107
|
+
return this.#handler.onData(chunk)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @see {DispatchHandlers.onComplete}
|
|
115
|
+
*
|
|
116
|
+
* @param {string[] | null} rawTrailers
|
|
117
|
+
*/
|
|
118
|
+
onComplete (rawTrailers) {
|
|
119
|
+
if (this.#successful) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof this.#handler.onComplete === 'function') {
|
|
124
|
+
this.#handler.onComplete(rawTrailers)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @see {DispatchHandlers.onError}
|
|
130
|
+
*
|
|
131
|
+
* @param {Error} err
|
|
132
|
+
*/
|
|
133
|
+
onError (err) {
|
|
134
|
+
if (this.#successful) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this.#callback) {
|
|
139
|
+
this.#callback(false)
|
|
140
|
+
this.#callback = null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof this.#handler.onError === 'function') {
|
|
144
|
+
this.#handler.onError(err)
|
|
145
|
+
} else {
|
|
146
|
+
throw err
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = CacheRevalidationHandler
|
|
@@ -75,7 +75,8 @@ class RedirectHandler {
|
|
|
75
75
|
this.opts.body &&
|
|
76
76
|
typeof this.opts.body !== 'string' &&
|
|
77
77
|
!ArrayBuffer.isView(this.opts.body) &&
|
|
78
|
-
util.isIterable(this.opts.body)
|
|
78
|
+
util.isIterable(this.opts.body) &&
|
|
79
|
+
!util.isFormDataLike(this.opts.body)
|
|
79
80
|
) {
|
|
80
81
|
// TODO: Should we allow re-using iterable if !this.opts.idempotent
|
|
81
82
|
// or through some other flag?
|
|
@@ -227,9 +228,10 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
} else if (headers && typeof headers === 'object') {
|
|
230
|
-
|
|
231
|
+
const entries = typeof headers[Symbol.iterator] === 'function' ? headers : Object.entries(headers)
|
|
232
|
+
for (const [key, value] of entries) {
|
|
231
233
|
if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
|
|
232
|
-
ret.push(key,
|
|
234
|
+
ret.push(key, value)
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
237
|
} else {
|
|
@@ -229,7 +229,7 @@ class RetryHandler {
|
|
|
229
229
|
return false
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
const { start, size, end = size } = contentRange
|
|
232
|
+
const { start, size, end = size - 1 } = contentRange
|
|
233
233
|
|
|
234
234
|
assert(this.start === start, 'content-range mismatch')
|
|
235
235
|
assert(this.end == null || this.end === end, 'content-range mismatch')
|
|
@@ -252,7 +252,7 @@ class RetryHandler {
|
|
|
252
252
|
)
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
const { start, size, end = size } = range
|
|
255
|
+
const { start, size, end = size - 1 } = range
|
|
256
256
|
assert(
|
|
257
257
|
start != null && Number.isFinite(start),
|
|
258
258
|
'content-range mismatch'
|
|
@@ -266,7 +266,7 @@ class RetryHandler {
|
|
|
266
266
|
// We make our best to checkpoint the body for further range headers
|
|
267
267
|
if (this.end == null) {
|
|
268
268
|
const contentLength = headers['content-length']
|
|
269
|
-
this.end = contentLength != null ? Number(contentLength) : null
|
|
269
|
+
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
assert(Number.isFinite(this.start))
|