undici 7.0.0-alpha.3 → 7.0.0-alpha.5
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/Agent.md +14 -14
- package/docs/docs/api/BalancedPool.md +16 -16
- package/docs/docs/api/CacheStore.md +17 -14
- package/docs/docs/api/Client.md +11 -11
- package/docs/docs/api/Dispatcher.md +30 -10
- package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
- package/docs/docs/api/MockAgent.md +3 -3
- package/docs/docs/api/MockClient.md +5 -5
- package/docs/docs/api/MockPool.md +2 -2
- package/docs/docs/api/Pool.md +15 -15
- package/docs/docs/api/PoolStats.md +1 -1
- package/docs/docs/api/ProxyAgent.md +3 -3
- package/docs/docs/api/RetryHandler.md +2 -2
- package/docs/docs/api/WebSocket.md +1 -1
- package/docs/docs/api/api-lifecycle.md +11 -11
- 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 +106 -342
- 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 +126 -85
- 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 +213 -92
- package/lib/interceptor/dns.js +71 -48
- package/lib/util/cache.js +73 -13
- package/lib/util/timers.js +19 -1
- 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 +51 -54
- 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
|
|
@@ -4,22 +4,26 @@ const util = require('../core/util')
|
|
|
4
4
|
const DecoratorHandler = require('../handler/decorator-handler')
|
|
5
5
|
const {
|
|
6
6
|
parseCacheControlHeader,
|
|
7
|
-
parseVaryHeader
|
|
7
|
+
parseVaryHeader,
|
|
8
|
+
isEtagUsable
|
|
8
9
|
} = require('../util/cache')
|
|
10
|
+
const { nowAbsolute } = require('../util/timers.js')
|
|
11
|
+
|
|
12
|
+
function noop () {}
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Writes a response to a CacheStore and then passes it on to the next handler
|
|
12
16
|
*/
|
|
13
17
|
class CacheHandler extends DecoratorHandler {
|
|
14
18
|
/**
|
|
15
|
-
* @type {import('../../types/cache-interceptor.d.ts').default.
|
|
19
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
16
20
|
*/
|
|
17
|
-
#
|
|
21
|
+
#cacheKey
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
|
-
* @type {import('../../types/
|
|
24
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
|
|
21
25
|
*/
|
|
22
|
-
#
|
|
26
|
+
#store
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
|
|
@@ -27,25 +31,36 @@ class CacheHandler extends DecoratorHandler {
|
|
|
27
31
|
#handler
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
|
-
* @type {import('
|
|
34
|
+
* @type {import('node:stream').Writable | undefined}
|
|
31
35
|
*/
|
|
32
36
|
#writeStream
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
|
|
36
|
-
* @param {import('../../types/
|
|
40
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
37
41
|
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
38
42
|
*/
|
|
39
|
-
constructor (opts,
|
|
43
|
+
constructor (opts, cacheKey, handler) {
|
|
40
44
|
const { store } = opts
|
|
41
45
|
|
|
42
46
|
super(handler)
|
|
43
47
|
|
|
44
48
|
this.#store = store
|
|
45
|
-
this.#
|
|
49
|
+
this.#cacheKey = cacheKey
|
|
46
50
|
this.#handler = handler
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
onConnect (abort) {
|
|
54
|
+
if (this.#writeStream) {
|
|
55
|
+
this.#writeStream.destroy()
|
|
56
|
+
this.#writeStream = undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof this.#handler.onConnect === 'function') {
|
|
60
|
+
this.#handler.onConnect(abort)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
/**
|
|
50
65
|
* @see {DispatchHandlers.onHeaders}
|
|
51
66
|
*
|
|
@@ -61,72 +76,71 @@ class CacheHandler extends DecoratorHandler {
|
|
|
61
76
|
resume,
|
|
62
77
|
statusMessage
|
|
63
78
|
) {
|
|
64
|
-
const downstreamOnHeaders = () =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
const downstreamOnHeaders = () => {
|
|
80
|
+
if (typeof this.#handler.onHeaders === 'function') {
|
|
81
|
+
return this.#handler.onHeaders(
|
|
82
|
+
statusCode,
|
|
83
|
+
rawHeaders,
|
|
84
|
+
resume,
|
|
85
|
+
statusMessage
|
|
86
|
+
)
|
|
87
|
+
} else {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
70
91
|
|
|
71
92
|
if (
|
|
72
|
-
!util.safeHTTPMethods.includes(this.#
|
|
93
|
+
!util.safeHTTPMethods.includes(this.#cacheKey.method) &&
|
|
73
94
|
statusCode >= 200 &&
|
|
74
95
|
statusCode <= 399
|
|
75
96
|
) {
|
|
76
|
-
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-
|
|
77
|
-
// Try/catch for if it's synchronous
|
|
97
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
|
|
78
98
|
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) {
|
|
99
|
+
this.#store.delete(this.#cacheKey).catch?.(noop)
|
|
100
|
+
} catch {
|
|
89
101
|
// Fail silently
|
|
90
102
|
}
|
|
91
|
-
|
|
92
103
|
return downstreamOnHeaders()
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
const
|
|
106
|
+
const parsedRawHeaders = util.parseRawHeaders(rawHeaders)
|
|
107
|
+
const headers = util.parseHeaders(parsedRawHeaders)
|
|
96
108
|
|
|
97
109
|
const cacheControlHeader = headers['cache-control']
|
|
98
|
-
const
|
|
110
|
+
const isCacheFull = typeof this.#store.isFull !== 'undefined'
|
|
111
|
+
? this.#store.isFull
|
|
112
|
+
: false
|
|
99
113
|
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const contentLength = Number(contentLengthHeader)
|
|
106
|
-
if (!Number.isInteger(contentLength)) {
|
|
114
|
+
if (
|
|
115
|
+
!cacheControlHeader ||
|
|
116
|
+
isCacheFull
|
|
117
|
+
) {
|
|
118
|
+
// Don't have the cache control header or the cache is full
|
|
107
119
|
return downstreamOnHeaders()
|
|
108
120
|
}
|
|
109
|
-
|
|
110
121
|
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
|
|
111
122
|
if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
|
|
112
123
|
return downstreamOnHeaders()
|
|
113
124
|
}
|
|
114
125
|
|
|
115
|
-
const now =
|
|
126
|
+
const now = nowAbsolute()
|
|
116
127
|
const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
|
|
117
128
|
if (staleAt) {
|
|
118
|
-
const varyDirectives = headers.vary
|
|
119
|
-
? parseVaryHeader(headers.vary, this.#
|
|
129
|
+
const varyDirectives = this.#cacheKey.headers && headers.vary
|
|
130
|
+
? parseVaryHeader(headers.vary, this.#cacheKey.headers)
|
|
120
131
|
: undefined
|
|
121
132
|
const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
|
|
122
133
|
|
|
123
134
|
const strippedHeaders = stripNecessaryHeaders(
|
|
124
135
|
rawHeaders,
|
|
125
|
-
|
|
136
|
+
parsedRawHeaders,
|
|
126
137
|
cacheControlDirectives
|
|
127
138
|
)
|
|
128
139
|
|
|
129
|
-
|
|
140
|
+
/**
|
|
141
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
|
|
142
|
+
*/
|
|
143
|
+
const value = {
|
|
130
144
|
statusCode,
|
|
131
145
|
statusMessage,
|
|
132
146
|
rawHeaders: strippedHeaders,
|
|
@@ -134,21 +148,33 @@ class CacheHandler extends DecoratorHandler {
|
|
|
134
148
|
cachedAt: now,
|
|
135
149
|
staleAt,
|
|
136
150
|
deleteAt
|
|
137
|
-
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (typeof headers.etag === 'string' && isEtagUsable(headers.etag)) {
|
|
154
|
+
value.etag = headers.etag
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
|
|
138
158
|
|
|
139
159
|
if (this.#writeStream) {
|
|
140
|
-
this
|
|
141
|
-
this.#writeStream
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
const handler = this
|
|
161
|
+
this.#writeStream
|
|
162
|
+
.on('drain', resume)
|
|
163
|
+
.on('error', function () {
|
|
164
|
+
// TODO (fix): Make error somehow observable?
|
|
165
|
+
})
|
|
166
|
+
.on('close', function () {
|
|
167
|
+
if (handler.#writeStream === this) {
|
|
168
|
+
handler.#writeStream = undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// TODO (fix): Should we resume even if was paused downstream?
|
|
172
|
+
resume()
|
|
173
|
+
})
|
|
145
174
|
}
|
|
146
175
|
}
|
|
147
176
|
|
|
148
|
-
|
|
149
|
-
return downstreamOnHeaders()
|
|
150
|
-
}
|
|
151
|
-
return false
|
|
177
|
+
return downstreamOnHeaders()
|
|
152
178
|
}
|
|
153
179
|
|
|
154
180
|
/**
|
|
@@ -178,10 +204,6 @@ class CacheHandler extends DecoratorHandler {
|
|
|
178
204
|
*/
|
|
179
205
|
onComplete (rawTrailers) {
|
|
180
206
|
if (this.#writeStream) {
|
|
181
|
-
if (rawTrailers) {
|
|
182
|
-
this.#writeStream.rawTrailers = rawTrailers
|
|
183
|
-
}
|
|
184
|
-
|
|
185
207
|
this.#writeStream.end()
|
|
186
208
|
}
|
|
187
209
|
|
|
@@ -211,7 +233,7 @@ class CacheHandler extends DecoratorHandler {
|
|
|
211
233
|
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
212
234
|
*
|
|
213
235
|
* @param {number} statusCode
|
|
214
|
-
* @param {Record<string, string>} headers
|
|
236
|
+
* @param {Record<string, string | string[]>} headers
|
|
215
237
|
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
216
238
|
*/
|
|
217
239
|
function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
@@ -223,7 +245,6 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
|
223
245
|
}
|
|
224
246
|
|
|
225
247
|
if (
|
|
226
|
-
!cacheControlDirectives.public ||
|
|
227
248
|
cacheControlDirectives.private === true ||
|
|
228
249
|
cacheControlDirectives['no-cache'] === true ||
|
|
229
250
|
cacheControlDirectives['no-store']
|
|
@@ -237,7 +258,11 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
|
237
258
|
}
|
|
238
259
|
|
|
239
260
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
240
|
-
if (headers
|
|
261
|
+
if (headers.authorization) {
|
|
262
|
+
if (!cacheControlDirectives.public || typeof headers.authorization !== 'string') {
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
|
|
241
266
|
if (
|
|
242
267
|
Array.isArray(cacheControlDirectives['no-cache']) &&
|
|
243
268
|
cacheControlDirectives['no-cache'].includes('authorization')
|
|
@@ -282,9 +307,12 @@ function determineStaleAt (now, headers, cacheControlDirectives) {
|
|
|
282
307
|
return now + (maxAge * 1000)
|
|
283
308
|
}
|
|
284
309
|
|
|
285
|
-
if (headers.expire) {
|
|
310
|
+
if (headers.expire && typeof headers.expire === 'string') {
|
|
286
311
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
|
|
287
|
-
|
|
312
|
+
const expiresDate = new Date(headers.expire)
|
|
313
|
+
if (expiresDate instanceof Date && !isNaN(expiresDate)) {
|
|
314
|
+
return now + (nowAbsolute() - expiresDate.getTime())
|
|
315
|
+
}
|
|
288
316
|
}
|
|
289
317
|
|
|
290
318
|
return undefined
|
|
@@ -306,11 +334,11 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
|
306
334
|
/**
|
|
307
335
|
* Strips headers required to be removed in cached responses
|
|
308
336
|
* @param {Buffer[]} rawHeaders
|
|
309
|
-
* @param {
|
|
337
|
+
* @param {string[]} parsedRawHeaders
|
|
310
338
|
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
311
|
-
* @returns {
|
|
339
|
+
* @returns {Buffer[]}
|
|
312
340
|
*/
|
|
313
|
-
function stripNecessaryHeaders (rawHeaders,
|
|
341
|
+
function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirectives) {
|
|
314
342
|
const headersToRemove = ['connection']
|
|
315
343
|
|
|
316
344
|
if (Array.isArray(cacheControlDirectives['no-cache'])) {
|
|
@@ -321,39 +349,52 @@ function stripNecessaryHeaders (rawHeaders, parsedHeaders, cacheControlDirective
|
|
|
321
349
|
headersToRemove.push(...cacheControlDirectives['private'])
|
|
322
350
|
}
|
|
323
351
|
|
|
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
352
|
let strippedHeaders
|
|
330
353
|
|
|
331
|
-
|
|
332
|
-
for (let i = 0; i <
|
|
333
|
-
const
|
|
354
|
+
let offset = 0
|
|
355
|
+
for (let i = 0; i < parsedRawHeaders.length; i += 2) {
|
|
356
|
+
const headerName = parsedRawHeaders[i]
|
|
334
357
|
|
|
335
|
-
if (headersToRemove.
|
|
336
|
-
// We have
|
|
358
|
+
if (headersToRemove.includes(headerName)) {
|
|
359
|
+
// We have at least one header we want to remove
|
|
337
360
|
if (!strippedHeaders) {
|
|
338
|
-
// This is the first header we want to remove, let's create the
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
361
|
+
// This is the first header we want to remove, let's create the array
|
|
362
|
+
// Since we're stripping headers, this will over allocate. We'll trim
|
|
363
|
+
// it later.
|
|
364
|
+
strippedHeaders = new Array(parsedRawHeaders.length)
|
|
365
|
+
|
|
366
|
+
// Backfill the previous headers into it
|
|
367
|
+
for (let j = 0; j < i; j += 2) {
|
|
368
|
+
strippedHeaders[j] = parsedRawHeaders[j]
|
|
369
|
+
strippedHeaders[j + 1] = parsedRawHeaders[j + 1]
|
|
344
370
|
}
|
|
345
371
|
}
|
|
346
372
|
|
|
373
|
+
// We can't map indices 1:1 from stripped headers to rawHeaders without
|
|
374
|
+
// creating holes (if we skip a header, we now have two holes where at
|
|
375
|
+
// element should be). So, let's keep an offset to keep strippedHeaders
|
|
376
|
+
// flattened. We can also use this at the end for trimming the empty
|
|
377
|
+
// elements off of strippedHeaders.
|
|
378
|
+
offset += 2
|
|
379
|
+
|
|
347
380
|
continue
|
|
348
381
|
}
|
|
349
382
|
|
|
350
|
-
//
|
|
383
|
+
// We want to keep this header. Let's add it to strippedHeaders if it exists
|
|
351
384
|
if (strippedHeaders) {
|
|
352
|
-
strippedHeaders[
|
|
385
|
+
strippedHeaders[i - offset] = parsedRawHeaders[i]
|
|
386
|
+
strippedHeaders[i + 1 - offset] = parsedRawHeaders[i + 1]
|
|
353
387
|
}
|
|
354
388
|
}
|
|
355
389
|
|
|
356
|
-
|
|
390
|
+
if (strippedHeaders) {
|
|
391
|
+
// Trim off the empty values at the end
|
|
392
|
+
strippedHeaders.length -= offset
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return strippedHeaders
|
|
396
|
+
? util.encodeRawHeaders(strippedHeaders)
|
|
397
|
+
: rawHeaders
|
|
357
398
|
}
|
|
358
399
|
|
|
359
400
|
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
|
}
|