undici 7.0.0-alpha.3 → 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 +2 -1
- package/docs/docs/api/BalancedPool.md +1 -1
- package/docs/docs/api/CacheStore.md +16 -32
- package/docs/docs/api/Dispatcher.md +22 -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 +2 -1
- package/lib/api/api-request.js +1 -1
- package/lib/cache/memory-cache-store.js +108 -200
- package/lib/core/connect.js +5 -0
- package/lib/core/request.js +2 -2
- package/lib/core/util.js +13 -40
- package/lib/dispatcher/client-h2.js +53 -33
- package/lib/handler/cache-handler.js +112 -82
- package/lib/handler/cache-revalidation-handler.js +45 -13
- package/lib/handler/redirect-handler.js +5 -3
- package/lib/handler/retry-handler.js +3 -3
- package/lib/interceptor/cache.js +115 -94
- package/lib/interceptor/dns.js +71 -48
- package/lib/util/cache.js +38 -13
- package/lib/web/cookies/index.js +12 -1
- package/lib/web/cookies/parse.js +6 -1
- package/lib/web/fetch/body.js +1 -5
- package/lib/web/fetch/formdata-parser.js +70 -43
- package/lib/web/fetch/headers.js +1 -1
- package/lib/web/fetch/index.js +4 -6
- package/lib/web/fetch/webidl.js +12 -4
- package/package.json +2 -3
- package/types/cache-interceptor.d.ts +36 -32
- package/types/cookies.d.ts +2 -0
- package/types/dispatcher.d.ts +1 -1
- package/types/index.d.ts +0 -1
- package/types/interceptors.d.ts +0 -1
|
@@ -144,7 +144,7 @@ function resumeH2 (client) {
|
|
|
144
144
|
const socket = client[kSocket]
|
|
145
145
|
|
|
146
146
|
if (socket?.destroyed === false) {
|
|
147
|
-
if (client[kSize] === 0
|
|
147
|
+
if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
|
|
148
148
|
socket.unref()
|
|
149
149
|
client[kHTTP2Session].unref()
|
|
150
150
|
} else {
|
|
@@ -184,17 +184,19 @@ function onHttp2SessionEnd () {
|
|
|
184
184
|
* @param {number} errorCode
|
|
185
185
|
*/
|
|
186
186
|
function onHttp2SessionGoAway (errorCode) {
|
|
187
|
-
//
|
|
187
|
+
// TODO(mcollina): Verify if GOAWAY implements the spec correctly:
|
|
188
|
+
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
|
|
189
|
+
// Specifically, we do not verify the "valid" stream id.
|
|
190
|
+
|
|
188
191
|
const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket]))
|
|
189
192
|
const client = this[kClient]
|
|
190
193
|
|
|
191
194
|
client[kSocket] = null
|
|
192
195
|
client[kHTTPContext] = null
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
197
|
+
// this is an HTTP2 session
|
|
198
|
+
this.close()
|
|
199
|
+
this[kHTTP2Session] = null
|
|
198
200
|
|
|
199
201
|
util.destroy(this[kSocket], err)
|
|
200
202
|
|
|
@@ -218,7 +220,8 @@ function onHttp2SessionClose () {
|
|
|
218
220
|
|
|
219
221
|
const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
|
|
220
222
|
|
|
221
|
-
client[
|
|
223
|
+
client[kSocket] = null
|
|
224
|
+
client[kHTTPContext] = null
|
|
222
225
|
|
|
223
226
|
if (client.destroyed) {
|
|
224
227
|
assert(client[kPending] === 0)
|
|
@@ -238,6 +241,7 @@ function onHttp2SocketClose () {
|
|
|
238
241
|
const client = this[kHTTP2Session][kClient]
|
|
239
242
|
|
|
240
243
|
client[kSocket] = null
|
|
244
|
+
client[kHTTPContext] = null
|
|
241
245
|
|
|
242
246
|
if (this[kHTTP2Session] !== null) {
|
|
243
247
|
this[kHTTP2Session].destroy(err)
|
|
@@ -301,7 +305,7 @@ function writeH2 (client, request) {
|
|
|
301
305
|
}
|
|
302
306
|
|
|
303
307
|
/** @type {import('node:http2').ClientHttp2Stream} */
|
|
304
|
-
let stream
|
|
308
|
+
let stream = null
|
|
305
309
|
|
|
306
310
|
const { hostname, port } = client[kUrl]
|
|
307
311
|
|
|
@@ -318,14 +322,21 @@ function writeH2 (client, request) {
|
|
|
318
322
|
util.errorRequest(client, request, err)
|
|
319
323
|
|
|
320
324
|
if (stream != null) {
|
|
321
|
-
|
|
325
|
+
// Some chunks might still come after abort,
|
|
326
|
+
// let's ignore them
|
|
327
|
+
stream.removeAllListeners('data')
|
|
328
|
+
|
|
329
|
+
// On Abort, we close the stream to send RST_STREAM frame
|
|
330
|
+
stream.close()
|
|
331
|
+
|
|
332
|
+
// We move the running index to the next request
|
|
333
|
+
client[kOnError](err)
|
|
334
|
+
client[kResume]()
|
|
322
335
|
}
|
|
323
336
|
|
|
324
337
|
// We do not destroy the socket as we can continue using the session
|
|
325
338
|
// the stream gets destroyed and the session remains to create new streams
|
|
326
339
|
util.destroy(body, err)
|
|
327
|
-
client[kQueue][client[kRunningIdx]++] = null
|
|
328
|
-
client[kResume]()
|
|
329
340
|
}
|
|
330
341
|
|
|
331
342
|
try {
|
|
@@ -348,7 +359,7 @@ function writeH2 (client, request) {
|
|
|
348
359
|
// We disabled endStream to allow the user to write to the stream
|
|
349
360
|
stream = session.request(headers, { endStream: false, signal })
|
|
350
361
|
|
|
351
|
-
if (
|
|
362
|
+
if (!stream.pending) {
|
|
352
363
|
request.onUpgrade(null, null, stream)
|
|
353
364
|
++session[kOpenStreams]
|
|
354
365
|
client[kQueue][client[kRunningIdx]++] = null
|
|
@@ -438,6 +449,7 @@ function writeH2 (client, request) {
|
|
|
438
449
|
endStream: shouldEndStream,
|
|
439
450
|
signal
|
|
440
451
|
})
|
|
452
|
+
|
|
441
453
|
writeBodyH2()
|
|
442
454
|
}
|
|
443
455
|
|
|
@@ -454,46 +466,52 @@ function writeH2 (client, request) {
|
|
|
454
466
|
// for those scenarios, best effort is to destroy the stream immediately
|
|
455
467
|
// as there's no value to keep it open.
|
|
456
468
|
if (request.aborted) {
|
|
457
|
-
|
|
458
|
-
util.errorRequest(client, request, err)
|
|
459
|
-
util.destroy(stream, err)
|
|
469
|
+
stream.removeAllListeners('data')
|
|
460
470
|
return
|
|
461
471
|
}
|
|
462
472
|
|
|
463
473
|
if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
|
|
464
474
|
stream.pause()
|
|
465
475
|
}
|
|
476
|
+
})
|
|
466
477
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
})
|
|
478
|
+
stream.on('data', (chunk) => {
|
|
479
|
+
if (request.onData(chunk) === false) {
|
|
480
|
+
stream.pause()
|
|
481
|
+
}
|
|
472
482
|
})
|
|
473
483
|
|
|
474
|
-
stream.once('end', () => {
|
|
484
|
+
stream.once('end', (err) => {
|
|
485
|
+
stream.removeAllListeners('data')
|
|
475
486
|
// When state is null, it means we haven't consumed body and the stream still do not have
|
|
476
487
|
// a state.
|
|
477
488
|
// Present specially when using pipeline or stream
|
|
478
489
|
if (stream.state?.state == null || stream.state.state < 6) {
|
|
479
|
-
request
|
|
480
|
-
|
|
490
|
+
// Do not complete the request if it was aborted
|
|
491
|
+
if (!request.aborted) {
|
|
492
|
+
request.onComplete([])
|
|
493
|
+
}
|
|
481
494
|
|
|
482
|
-
|
|
495
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
496
|
+
client[kResume]()
|
|
497
|
+
} else {
|
|
483
498
|
// Stream is closed or half-closed-remote (6), decrement counter and cleanup
|
|
484
499
|
// It does not have sense to continue working with the stream as we do not
|
|
485
500
|
// have yet RST_STREAM support on client-side
|
|
501
|
+
--session[kOpenStreams]
|
|
502
|
+
if (session[kOpenStreams] === 0) {
|
|
503
|
+
session.unref()
|
|
504
|
+
}
|
|
486
505
|
|
|
487
|
-
|
|
506
|
+
abort(err ?? new InformationalError('HTTP/2: stream half-closed (remote)'))
|
|
507
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
508
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
509
|
+
client[kResume]()
|
|
488
510
|
}
|
|
489
|
-
|
|
490
|
-
abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
|
|
491
|
-
client[kQueue][client[kRunningIdx]++] = null
|
|
492
|
-
client[kPendingIdx] = client[kRunningIdx]
|
|
493
|
-
client[kResume]()
|
|
494
511
|
})
|
|
495
512
|
|
|
496
513
|
stream.once('close', () => {
|
|
514
|
+
stream.removeAllListeners('data')
|
|
497
515
|
session[kOpenStreams] -= 1
|
|
498
516
|
if (session[kOpenStreams] === 0) {
|
|
499
517
|
session.unref()
|
|
@@ -501,16 +519,18 @@ function writeH2 (client, request) {
|
|
|
501
519
|
})
|
|
502
520
|
|
|
503
521
|
stream.once('error', function (err) {
|
|
522
|
+
stream.removeAllListeners('data')
|
|
504
523
|
abort(err)
|
|
505
524
|
})
|
|
506
525
|
|
|
507
526
|
stream.once('frameError', (type, code) => {
|
|
527
|
+
stream.removeAllListeners('data')
|
|
508
528
|
abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
|
|
509
529
|
})
|
|
510
530
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
531
|
+
stream.on('aborted', () => {
|
|
532
|
+
stream.removeAllListeners('data')
|
|
533
|
+
})
|
|
514
534
|
|
|
515
535
|
// stream.on('timeout', () => {
|
|
516
536
|
// // TODO(HTTP/2): Support timeout
|
|
@@ -7,19 +7,21 @@ const {
|
|
|
7
7
|
parseVaryHeader
|
|
8
8
|
} = require('../util/cache')
|
|
9
9
|
|
|
10
|
+
function noop () {}
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Writes a response to a CacheStore and then passes it on to the next handler
|
|
12
14
|
*/
|
|
13
15
|
class CacheHandler extends DecoratorHandler {
|
|
14
16
|
/**
|
|
15
|
-
* @type {import('../../types/cache-interceptor.d.ts').default.
|
|
17
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
16
18
|
*/
|
|
17
|
-
#
|
|
19
|
+
#cacheKey
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
* @type {import('../../types/
|
|
22
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
|
|
21
23
|
*/
|
|
22
|
-
#
|
|
24
|
+
#store
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
|
|
@@ -27,25 +29,36 @@ class CacheHandler extends DecoratorHandler {
|
|
|
27
29
|
#handler
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
* @type {import('
|
|
32
|
+
* @type {import('node:stream').Writable | undefined}
|
|
31
33
|
*/
|
|
32
34
|
#writeStream
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
|
|
36
|
-
* @param {import('../../types/
|
|
38
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
37
39
|
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
38
40
|
*/
|
|
39
|
-
constructor (opts,
|
|
41
|
+
constructor (opts, cacheKey, handler) {
|
|
40
42
|
const { store } = opts
|
|
41
43
|
|
|
42
44
|
super(handler)
|
|
43
45
|
|
|
44
46
|
this.#store = store
|
|
45
|
-
this.#
|
|
47
|
+
this.#cacheKey = cacheKey
|
|
46
48
|
this.#handler = handler
|
|
47
49
|
}
|
|
48
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
|
+
|
|
49
62
|
/**
|
|
50
63
|
* @see {DispatchHandlers.onHeaders}
|
|
51
64
|
*
|
|
@@ -61,52 +74,48 @@ class CacheHandler extends DecoratorHandler {
|
|
|
61
74
|
resume,
|
|
62
75
|
statusMessage
|
|
63
76
|
) {
|
|
64
|
-
const downstreamOnHeaders = () =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
89
|
|
|
71
90
|
if (
|
|
72
|
-
!util.safeHTTPMethods.includes(this.#
|
|
91
|
+
!util.safeHTTPMethods.includes(this.#cacheKey.method) &&
|
|
73
92
|
statusCode >= 200 &&
|
|
74
93
|
statusCode <= 399
|
|
75
94
|
) {
|
|
76
|
-
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-
|
|
77
|
-
// Try/catch for if it's synchronous
|
|
95
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
|
|
78
96
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
result &&
|
|
82
|
-
typeof result.catch === 'function' &&
|
|
83
|
-
typeof this.#handler.onError === 'function'
|
|
84
|
-
) {
|
|
85
|
-
// Fail silently
|
|
86
|
-
result.catch(_ => {})
|
|
87
|
-
}
|
|
88
|
-
} catch (err) {
|
|
97
|
+
this.#store.delete(this.#cacheKey).catch?.(noop)
|
|
98
|
+
} catch {
|
|
89
99
|
// Fail silently
|
|
90
100
|
}
|
|
91
|
-
|
|
92
101
|
return downstreamOnHeaders()
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
const
|
|
104
|
+
const parsedRawHeaders = util.parseRawHeaders(rawHeaders)
|
|
105
|
+
const headers = util.parseHeaders(parsedRawHeaders)
|
|
96
106
|
|
|
97
107
|
const cacheControlHeader = headers['cache-control']
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Don't have the headers we need, can't cache
|
|
102
|
-
return downstreamOnHeaders()
|
|
103
|
-
}
|
|
108
|
+
const isCacheFull = typeof this.#store.isFull !== 'undefined'
|
|
109
|
+
? this.#store.isFull
|
|
110
|
+
: false
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
if (
|
|
113
|
+
!cacheControlHeader ||
|
|
114
|
+
isCacheFull
|
|
115
|
+
) {
|
|
116
|
+
// Don't have the cache control header or the cache is full
|
|
107
117
|
return downstreamOnHeaders()
|
|
108
118
|
}
|
|
109
|
-
|
|
110
119
|
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
|
|
111
120
|
if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
|
|
112
121
|
return downstreamOnHeaders()
|
|
@@ -115,18 +124,18 @@ class CacheHandler extends DecoratorHandler {
|
|
|
115
124
|
const now = Date.now()
|
|
116
125
|
const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
|
|
117
126
|
if (staleAt) {
|
|
118
|
-
const varyDirectives = headers.vary
|
|
119
|
-
? parseVaryHeader(headers.vary, this.#
|
|
127
|
+
const varyDirectives = this.#cacheKey.headers && headers.vary
|
|
128
|
+
? parseVaryHeader(headers.vary, this.#cacheKey.headers)
|
|
120
129
|
: undefined
|
|
121
130
|
const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
|
|
122
131
|
|
|
123
132
|
const strippedHeaders = stripNecessaryHeaders(
|
|
124
133
|
rawHeaders,
|
|
125
|
-
|
|
134
|
+
parsedRawHeaders,
|
|
126
135
|
cacheControlDirectives
|
|
127
136
|
)
|
|
128
137
|
|
|
129
|
-
this.#writeStream = this.#store.createWriteStream(this.#
|
|
138
|
+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, {
|
|
130
139
|
statusCode,
|
|
131
140
|
statusMessage,
|
|
132
141
|
rawHeaders: strippedHeaders,
|
|
@@ -137,18 +146,24 @@ class CacheHandler extends DecoratorHandler {
|
|
|
137
146
|
})
|
|
138
147
|
|
|
139
148
|
if (this.#writeStream) {
|
|
140
|
-
this
|
|
141
|
-
this.#writeStream
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
})
|
|
145
163
|
}
|
|
146
164
|
}
|
|
147
165
|
|
|
148
|
-
|
|
149
|
-
return downstreamOnHeaders()
|
|
150
|
-
}
|
|
151
|
-
return false
|
|
166
|
+
return downstreamOnHeaders()
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
/**
|
|
@@ -178,10 +193,6 @@ class CacheHandler extends DecoratorHandler {
|
|
|
178
193
|
*/
|
|
179
194
|
onComplete (rawTrailers) {
|
|
180
195
|
if (this.#writeStream) {
|
|
181
|
-
if (rawTrailers) {
|
|
182
|
-
this.#writeStream.rawTrailers = rawTrailers
|
|
183
|
-
}
|
|
184
|
-
|
|
185
196
|
this.#writeStream.end()
|
|
186
197
|
}
|
|
187
198
|
|
|
@@ -211,7 +222,7 @@ class CacheHandler extends DecoratorHandler {
|
|
|
211
222
|
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
212
223
|
*
|
|
213
224
|
* @param {number} statusCode
|
|
214
|
-
* @param {Record<string, string>} headers
|
|
225
|
+
* @param {Record<string, string | string[]>} headers
|
|
215
226
|
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
216
227
|
*/
|
|
217
228
|
function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
@@ -223,7 +234,6 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
if (
|
|
226
|
-
!cacheControlDirectives.public ||
|
|
227
237
|
cacheControlDirectives.private === true ||
|
|
228
238
|
cacheControlDirectives['no-cache'] === true ||
|
|
229
239
|
cacheControlDirectives['no-store']
|
|
@@ -237,7 +247,11 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
|
237
247
|
}
|
|
238
248
|
|
|
239
249
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
240
|
-
if (headers
|
|
250
|
+
if (headers.authorization) {
|
|
251
|
+
if (!cacheControlDirectives.public || typeof headers.authorization !== 'string') {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
241
255
|
if (
|
|
242
256
|
Array.isArray(cacheControlDirectives['no-cache']) &&
|
|
243
257
|
cacheControlDirectives['no-cache'].includes('authorization')
|
|
@@ -282,9 +296,12 @@ function determineStaleAt (now, headers, cacheControlDirectives) {
|
|
|
282
296
|
return now + (maxAge * 1000)
|
|
283
297
|
}
|
|
284
298
|
|
|
285
|
-
if (headers.expire) {
|
|
299
|
+
if (headers.expire && typeof headers.expire === 'string') {
|
|
286
300
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
|
|
287
|
-
|
|
301
|
+
const expiresDate = new Date(headers.expire)
|
|
302
|
+
if (expiresDate instanceof Date && !isNaN(expiresDate)) {
|
|
303
|
+
return now + (Date.now() - expiresDate.getTime())
|
|
304
|
+
}
|
|
288
305
|
}
|
|
289
306
|
|
|
290
307
|
return undefined
|
|
@@ -306,11 +323,11 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
|
306
323
|
/**
|
|
307
324
|
* Strips headers required to be removed in cached responses
|
|
308
325
|
* @param {Buffer[]} rawHeaders
|
|
309
|
-
* @param {
|
|
326
|
+
* @param {string[]} parsedRawHeaders
|
|
310
327
|
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
311
|
-
* @returns {
|
|
328
|
+
* @returns {Buffer[]}
|
|
312
329
|
*/
|
|
313
|
-
function stripNecessaryHeaders (rawHeaders,
|
|
330
|
+
function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirectives) {
|
|
314
331
|
const headersToRemove = ['connection']
|
|
315
332
|
|
|
316
333
|
if (Array.isArray(cacheControlDirectives['no-cache'])) {
|
|
@@ -321,39 +338,52 @@ function stripNecessaryHeaders (rawHeaders, parsedHeaders, cacheControlDirective
|
|
|
321
338
|
headersToRemove.push(...cacheControlDirectives['private'])
|
|
322
339
|
}
|
|
323
340
|
|
|
324
|
-
/**
|
|
325
|
-
* These are the headers that are okay to cache. If this is assigned, we need
|
|
326
|
-
* to remake the buffer representation of the headers
|
|
327
|
-
* @type {Record<string, string | string[]> | undefined}
|
|
328
|
-
*/
|
|
329
341
|
let strippedHeaders
|
|
330
342
|
|
|
331
|
-
|
|
332
|
-
for (let i = 0; i <
|
|
333
|
-
const
|
|
343
|
+
let offset = 0
|
|
344
|
+
for (let i = 0; i < parsedRawHeaders.length; i += 2) {
|
|
345
|
+
const headerName = parsedRawHeaders[i]
|
|
334
346
|
|
|
335
|
-
if (headersToRemove.
|
|
336
|
-
// We have
|
|
347
|
+
if (headersToRemove.includes(headerName)) {
|
|
348
|
+
// We have at least one header we want to remove
|
|
337
349
|
if (!strippedHeaders) {
|
|
338
|
-
// This is the first header we want to remove, let's create the
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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]
|
|
344
359
|
}
|
|
345
360
|
}
|
|
346
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
|
+
|
|
347
369
|
continue
|
|
348
370
|
}
|
|
349
371
|
|
|
350
|
-
//
|
|
372
|
+
// We want to keep this header. Let's add it to strippedHeaders if it exists
|
|
351
373
|
if (strippedHeaders) {
|
|
352
|
-
strippedHeaders[
|
|
374
|
+
strippedHeaders[i - offset] = parsedRawHeaders[i]
|
|
375
|
+
strippedHeaders[i + 1 - offset] = parsedRawHeaders[i + 1]
|
|
353
376
|
}
|
|
354
377
|
}
|
|
355
378
|
|
|
356
|
-
|
|
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
|
|
357
387
|
}
|
|
358
388
|
|
|
359
389
|
module.exports = CacheHandler
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
3
4
|
const DecoratorHandler = require('../handler/decorator-handler')
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -19,29 +20,36 @@ const DecoratorHandler = require('../handler/decorator-handler')
|
|
|
19
20
|
class CacheRevalidationHandler extends DecoratorHandler {
|
|
20
21
|
#successful = false
|
|
21
22
|
/**
|
|
22
|
-
* @type {(() => void)}
|
|
23
|
+
* @type {((boolean) => void) | null}
|
|
23
24
|
*/
|
|
24
|
-
#
|
|
25
|
+
#callback
|
|
25
26
|
/**
|
|
26
27
|
* @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)}
|
|
27
28
|
*/
|
|
28
29
|
#handler
|
|
29
30
|
|
|
31
|
+
#abort
|
|
32
|
+
|
|
30
33
|
/**
|
|
31
|
-
* @param {() => void}
|
|
34
|
+
* @param {(boolean) => void} callback Function to call if the cached value is valid
|
|
32
35
|
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
33
36
|
*/
|
|
34
|
-
constructor (
|
|
35
|
-
if (typeof
|
|
36
|
-
throw new TypeError('
|
|
37
|
+
constructor (callback, handler) {
|
|
38
|
+
if (typeof callback !== 'function') {
|
|
39
|
+
throw new TypeError('callback must be a function')
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
super(handler)
|
|
40
43
|
|
|
41
|
-
this.#
|
|
44
|
+
this.#callback = callback
|
|
42
45
|
this.#handler = handler
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
onConnect (abort) {
|
|
49
|
+
this.#successful = false
|
|
50
|
+
this.#abort = abort
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
/**
|
|
46
54
|
* @see {DispatchHandlers.onHeaders}
|
|
47
55
|
*
|
|
@@ -57,13 +65,21 @@ class CacheRevalidationHandler extends DecoratorHandler {
|
|
|
57
65
|
resume,
|
|
58
66
|
statusMessage
|
|
59
67
|
) {
|
|
68
|
+
assert(this.#callback != null)
|
|
69
|
+
|
|
60
70
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
this.#successful = statusCode === 304
|
|
72
|
+
this.#callback(this.#successful)
|
|
73
|
+
this.#callback = null
|
|
74
|
+
|
|
75
|
+
if (this.#successful) {
|
|
64
76
|
return true
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
if (typeof this.#handler.onConnect === 'function') {
|
|
80
|
+
this.#handler.onConnect(this.#abort)
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
if (typeof this.#handler.onHeaders === 'function') {
|
|
68
84
|
return this.#handler.onHeaders(
|
|
69
85
|
statusCode,
|
|
@@ -72,7 +88,8 @@ class CacheRevalidationHandler extends DecoratorHandler {
|
|
|
72
88
|
statusMessage
|
|
73
89
|
)
|
|
74
90
|
}
|
|
75
|
-
|
|
91
|
+
|
|
92
|
+
return true
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/**
|
|
@@ -90,7 +107,7 @@ class CacheRevalidationHandler extends DecoratorHandler {
|
|
|
90
107
|
return this.#handler.onData(chunk)
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
return
|
|
110
|
+
return true
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
/**
|
|
@@ -99,7 +116,11 @@ class CacheRevalidationHandler extends DecoratorHandler {
|
|
|
99
116
|
* @param {string[] | null} rawTrailers
|
|
100
117
|
*/
|
|
101
118
|
onComplete (rawTrailers) {
|
|
102
|
-
if (
|
|
119
|
+
if (this.#successful) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof this.#handler.onComplete === 'function') {
|
|
103
124
|
this.#handler.onComplete(rawTrailers)
|
|
104
125
|
}
|
|
105
126
|
}
|
|
@@ -110,8 +131,19 @@ class CacheRevalidationHandler extends DecoratorHandler {
|
|
|
110
131
|
* @param {Error} err
|
|
111
132
|
*/
|
|
112
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
|
+
|
|
113
143
|
if (typeof this.#handler.onError === 'function') {
|
|
114
144
|
this.#handler.onError(err)
|
|
145
|
+
} else {
|
|
146
|
+
throw err
|
|
115
147
|
}
|
|
116
148
|
}
|
|
117
149
|
}
|
|
@@ -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 {
|