undici 7.0.0-alpha.6 → 7.0.0-alpha.8

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.
@@ -1,5 +1,8 @@
1
1
  'use strict'
2
2
  const EventEmitter = require('node:events')
3
+ const WrapHandler = require('../handler/wrap-handler')
4
+
5
+ const wrapInterceptor = (dispatch) => (opts, handler) => dispatch(opts, WrapHandler.wrap(handler))
3
6
 
4
7
  class Dispatcher extends EventEmitter {
5
8
  dispatch () {
@@ -29,6 +32,7 @@ class Dispatcher extends EventEmitter {
29
32
  }
30
33
 
31
34
  dispatch = interceptor(dispatch)
35
+ dispatch = wrapInterceptor(dispatch)
32
36
 
33
37
  if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
34
38
  throw new TypeError('invalid interceptor')
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const util = require('../core/util')
4
- const DecoratorHandler = require('../handler/decorator-handler')
5
4
  const {
6
5
  parseCacheControlHeader,
7
6
  parseVaryHeader,
@@ -11,21 +10,33 @@ const {
11
10
  function noop () {}
12
11
 
13
12
  /**
14
- * Writes a response to a CacheStore and then passes it on to the next handler
13
+ * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
14
+ *
15
+ * @implements {DispatchHandler}
15
16
  */
16
- class CacheHandler extends DecoratorHandler {
17
+ class CacheHandler {
17
18
  /**
18
19
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
19
20
  */
20
21
  #cacheKey
21
22
 
23
+ /**
24
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
25
+ */
26
+ #cacheType
27
+
28
+ /**
29
+ * @type {number | undefined}
30
+ */
31
+ #cacheByDefault
32
+
22
33
  /**
23
34
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
24
35
  */
25
36
  #store
26
37
 
27
38
  /**
28
- * @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
39
+ * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
29
40
  */
30
41
  #handler
31
42
 
@@ -35,58 +46,41 @@ class CacheHandler extends DecoratorHandler {
35
46
  #writeStream
36
47
 
37
48
  /**
38
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
49
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
39
50
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
40
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
51
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
41
52
  */
42
- constructor (opts, cacheKey, handler) {
43
- const { store } = opts
44
-
45
- super(handler)
46
-
53
+ constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
47
54
  this.#store = store
55
+ this.#cacheType = type
56
+ this.#cacheByDefault = cacheByDefault
48
57
  this.#cacheKey = cacheKey
49
58
  this.#handler = handler
50
59
  }
51
60
 
52
- onConnect (abort) {
53
- if (this.#writeStream) {
54
- this.#writeStream.destroy()
55
- this.#writeStream = undefined
56
- }
61
+ onRequestStart (controller, context) {
62
+ this.#writeStream?.destroy()
63
+ this.#writeStream = undefined
64
+ this.#handler.onRequestStart?.(controller, context)
65
+ }
57
66
 
58
- if (typeof this.#handler.onConnect === 'function') {
59
- this.#handler.onConnect(abort)
60
- }
67
+ onRequestUpgrade (controller, statusCode, headers, socket) {
68
+ this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
61
69
  }
62
70
 
63
- /**
64
- * @see {DispatchHandlers.onHeaders}
65
- *
66
- * @param {number} statusCode
67
- * @param {Buffer[]} rawHeaders
68
- * @param {() => void} resume
69
- * @param {string} statusMessage
70
- * @returns {boolean}
71
- */
72
- onHeaders (
71
+ onResponseStart (
72
+ controller,
73
73
  statusCode,
74
- rawHeaders,
75
- resume,
76
- statusMessage
74
+ statusMessage,
75
+ headers
77
76
  ) {
78
- const downstreamOnHeaders = () => {
79
- if (typeof this.#handler.onHeaders === 'function') {
80
- return this.#handler.onHeaders(
81
- statusCode,
82
- rawHeaders,
83
- resume,
84
- statusMessage
85
- )
86
- } else {
87
- return true
88
- }
89
- }
77
+ const downstreamOnHeaders = () =>
78
+ this.#handler.onResponseStart?.(
79
+ controller,
80
+ statusCode,
81
+ statusMessage,
82
+ headers
83
+ )
90
84
 
91
85
  if (
92
86
  !util.safeHTTPMethods.includes(this.#cacheKey.method) &&
@@ -102,39 +96,49 @@ class CacheHandler extends DecoratorHandler {
102
96
  return downstreamOnHeaders()
103
97
  }
104
98
 
105
- const parsedRawHeaders = util.parseRawHeaders(rawHeaders)
106
- const headers = util.parseHeaders(parsedRawHeaders)
107
-
108
99
  const cacheControlHeader = headers['cache-control']
109
- const isCacheFull = typeof this.#store.isFull !== 'undefined'
110
- ? this.#store.isFull
111
- : false
112
-
113
- if (
114
- !cacheControlHeader ||
115
- isCacheFull
116
- ) {
100
+ if (!cacheControlHeader && !headers['expires'] && !this.#cacheByDefault) {
117
101
  // Don't have the cache control header or the cache is full
118
102
  return downstreamOnHeaders()
119
103
  }
120
- const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
121
- if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
104
+
105
+ const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
106
+ if (!canCacheResponse(this.#cacheType, statusCode, headers, cacheControlDirectives)) {
122
107
  return downstreamOnHeaders()
123
108
  }
124
109
 
110
+ const age = getAge(headers)
111
+
125
112
  const now = Date.now()
126
- const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
113
+ const staleAt = determineStaleAt(this.#cacheType, now, headers, cacheControlDirectives) ?? this.#cacheByDefault
127
114
  if (staleAt) {
128
- const varyDirectives = this.#cacheKey.headers && headers.vary
129
- ? parseVaryHeader(headers.vary, this.#cacheKey.headers)
130
- : undefined
131
- const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
132
-
133
- const strippedHeaders = stripNecessaryHeaders(
134
- rawHeaders,
135
- parsedRawHeaders,
136
- cacheControlDirectives
137
- )
115
+ let baseTime = now
116
+ if (headers['date']) {
117
+ const parsedDate = parseInt(headers['date'])
118
+ const date = new Date(isNaN(parsedDate) ? headers['date'] : parsedDate)
119
+ if (date instanceof Date && !isNaN(date)) {
120
+ baseTime = date.getTime()
121
+ }
122
+ }
123
+
124
+ const absoluteStaleAt = staleAt + baseTime
125
+
126
+ if (now >= absoluteStaleAt || (age && age >= staleAt)) {
127
+ // Response is already stale
128
+ return downstreamOnHeaders()
129
+ }
130
+
131
+ let varyDirectives
132
+ if (this.#cacheKey.headers && headers.vary) {
133
+ varyDirectives = parseVaryHeader(headers.vary, this.#cacheKey.headers)
134
+ if (!varyDirectives) {
135
+ // Parse error
136
+ return downstreamOnHeaders()
137
+ }
138
+ }
139
+
140
+ const deleteAt = determineDeleteAt(cacheControlDirectives, absoluteStaleAt)
141
+ const strippedHeaders = stripNecessaryHeaders(headers, cacheControlDirectives)
138
142
 
139
143
  /**
140
144
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
@@ -142,10 +146,11 @@ class CacheHandler extends DecoratorHandler {
142
146
  const value = {
143
147
  statusCode,
144
148
  statusMessage,
145
- rawHeaders: strippedHeaders,
149
+ headers: strippedHeaders,
146
150
  vary: varyDirectives,
147
- cachedAt: now,
148
- staleAt,
151
+ cacheControlDirectives,
152
+ cachedAt: age ? now - (age * 1000) : now,
153
+ staleAt: absoluteStaleAt,
149
154
  deleteAt
150
155
  }
151
156
 
@@ -158,9 +163,10 @@ class CacheHandler extends DecoratorHandler {
158
163
  if (this.#writeStream) {
159
164
  const handler = this
160
165
  this.#writeStream
161
- .on('drain', resume)
166
+ .on('drain', () => controller.resume())
162
167
  .on('error', function () {
163
168
  // TODO (fix): Make error somehow observable?
169
+ handler.#writeStream = undefined
164
170
  })
165
171
  .on('close', function () {
166
172
  if (handler.#writeStream === this) {
@@ -168,7 +174,7 @@ class CacheHandler extends DecoratorHandler {
168
174
  }
169
175
 
170
176
  // TODO (fix): Should we resume even if was paused downstream?
171
- resume()
177
+ controller.resume()
172
178
  })
173
179
  }
174
180
  }
@@ -176,83 +182,52 @@ class CacheHandler extends DecoratorHandler {
176
182
  return downstreamOnHeaders()
177
183
  }
178
184
 
179
- /**
180
- * @see {DispatchHandlers.onData}
181
- *
182
- * @param {Buffer} chunk
183
- * @returns {boolean}
184
- */
185
- onData (chunk) {
186
- let paused = false
187
-
188
- if (this.#writeStream) {
189
- paused ||= this.#writeStream.write(chunk) === false
190
- }
191
-
192
- if (typeof this.#handler.onData === 'function') {
193
- paused ||= this.#handler.onData(chunk) === false
185
+ onResponseData (controller, chunk) {
186
+ if (this.#writeStream?.write(chunk) === false) {
187
+ controller.pause()
194
188
  }
195
189
 
196
- return !paused
190
+ this.#handler.onResponseData?.(controller, chunk)
197
191
  }
198
192
 
199
- /**
200
- * @see {DispatchHandlers.onComplete}
201
- *
202
- * @param {string[] | null} rawTrailers
203
- */
204
- onComplete (rawTrailers) {
205
- if (this.#writeStream) {
206
- this.#writeStream.end()
207
- }
208
-
209
- if (typeof this.#handler.onComplete === 'function') {
210
- return this.#handler.onComplete(rawTrailers)
211
- }
193
+ onResponseEnd (controller, trailers) {
194
+ this.#writeStream?.end()
195
+ this.#handler.onResponseEnd?.(controller, trailers)
212
196
  }
213
197
 
214
- /**
215
- * @see {DispatchHandlers.onError}
216
- *
217
- * @param {Error} err
218
- */
219
- onError (err) {
220
- if (this.#writeStream) {
221
- this.#writeStream.destroy(err)
222
- this.#writeStream = undefined
223
- }
224
-
225
- if (typeof this.#handler.onError === 'function') {
226
- this.#handler.onError(err)
227
- }
198
+ onResponseError (controller, err) {
199
+ this.#writeStream?.destroy(err)
200
+ this.#writeStream = undefined
201
+ this.#handler.onResponseError?.(controller, err)
228
202
  }
229
203
  }
230
204
 
231
205
  /**
232
206
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
233
207
  *
208
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
234
209
  * @param {number} statusCode
235
210
  * @param {Record<string, string | string[]>} headers
236
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
211
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
237
212
  */
238
- function canCacheResponse (statusCode, headers, cacheControlDirectives) {
239
- if (
240
- statusCode !== 200 &&
241
- statusCode !== 307
242
- ) {
213
+ function canCacheResponse (cacheType, statusCode, headers, cacheControlDirectives) {
214
+ if (statusCode !== 200 && statusCode !== 307) {
243
215
  return false
244
216
  }
245
217
 
246
218
  if (
247
- cacheControlDirectives.private === true ||
248
219
  cacheControlDirectives['no-cache'] === true ||
249
220
  cacheControlDirectives['no-store']
250
221
  ) {
251
222
  return false
252
223
  }
253
224
 
225
+ if (cacheType === 'shared' && cacheControlDirectives.private === true) {
226
+ return false
227
+ }
228
+
254
229
  // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
255
- if (headers.vary === '*') {
230
+ if (headers.vary?.includes('*')) {
256
231
  return false
257
232
  }
258
233
 
@@ -281,64 +256,120 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
281
256
  }
282
257
 
283
258
  /**
259
+ * @param {Record<string, string | string[]>} headers
260
+ * @returns {number | undefined}
261
+ */
262
+ function getAge (headers) {
263
+ if (!headers.age) {
264
+ return undefined
265
+ }
266
+
267
+ const age = parseInt(Array.isArray(headers.age) ? headers.age[0] : headers.age)
268
+ if (isNaN(age) || age >= 2147483647) {
269
+ return undefined
270
+ }
271
+
272
+ return age
273
+ }
274
+
275
+ /**
276
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
284
277
  * @param {number} now
285
278
  * @param {Record<string, string | string[]>} headers
286
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
279
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
287
280
  *
288
281
  * @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
289
282
  */
290
- function determineStaleAt (now, headers, cacheControlDirectives) {
291
- // Prioritize s-maxage since we're a shared cache
292
- // s-maxage > max-age > Expire
293
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
294
- const sMaxAge = cacheControlDirectives['s-maxage']
295
- if (sMaxAge) {
296
- return now + (sMaxAge * 1000)
297
- }
298
-
299
- if (cacheControlDirectives.immutable) {
300
- // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
301
- return now + 31536000
283
+ function determineStaleAt (cacheType, now, headers, cacheControlDirectives) {
284
+ if (cacheType === 'shared') {
285
+ // Prioritize s-maxage since we're a shared cache
286
+ // s-maxage > max-age > Expire
287
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
288
+ const sMaxAge = cacheControlDirectives['s-maxage']
289
+ if (sMaxAge) {
290
+ return sMaxAge * 1000
291
+ }
302
292
  }
303
293
 
304
294
  const maxAge = cacheControlDirectives['max-age']
305
295
  if (maxAge) {
306
- return now + (maxAge * 1000)
296
+ return maxAge * 1000
307
297
  }
308
298
 
309
- if (headers.expire && typeof headers.expire === 'string') {
299
+ if (headers.expires && typeof headers.expires === 'string') {
310
300
  // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
311
- const expiresDate = new Date(headers.expire)
312
- if (expiresDate instanceof Date && !isNaN(expiresDate)) {
313
- return now + (Date.now() - expiresDate.getTime())
301
+ const expiresDate = new Date(headers.expires)
302
+ if (expiresDate instanceof Date && Number.isFinite(expiresDate.valueOf())) {
303
+ if (now >= expiresDate.getTime()) {
304
+ return undefined
305
+ }
306
+
307
+ return expiresDate.getTime() - now
314
308
  }
315
309
  }
316
310
 
311
+ if (cacheControlDirectives.immutable) {
312
+ // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
313
+ return 31536000
314
+ }
315
+
317
316
  return undefined
318
317
  }
319
318
 
320
319
  /**
321
- * @param {number} now
322
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
320
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
323
321
  * @param {number} staleAt
324
322
  */
325
- function determineDeleteAt (now, cacheControlDirectives, staleAt) {
323
+ function determineDeleteAt (cacheControlDirectives, staleAt) {
324
+ let staleWhileRevalidate = -Infinity
325
+ let staleIfError = -Infinity
326
+ let immutable = -Infinity
327
+
326
328
  if (cacheControlDirectives['stale-while-revalidate']) {
327
- return now + (cacheControlDirectives['stale-while-revalidate'] * 1000)
329
+ staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
330
+ }
331
+
332
+ if (cacheControlDirectives['stale-if-error']) {
333
+ staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
328
334
  }
329
335
 
330
- return staleAt
336
+ if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
337
+ immutable = 31536000
338
+ }
339
+
340
+ return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
331
341
  }
332
342
 
333
343
  /**
334
344
  * Strips headers required to be removed in cached responses
335
- * @param {Buffer[]} rawHeaders
336
- * @param {string[]} parsedRawHeaders
337
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
338
- * @returns {Buffer[]}
345
+ * @param {Record<string, string | string[]>} headers
346
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
347
+ * @returns {Record<string, string | string []>}
339
348
  */
340
- function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirectives) {
341
- const headersToRemove = ['connection']
349
+ function stripNecessaryHeaders (headers, cacheControlDirectives) {
350
+ const headersToRemove = [
351
+ 'connection',
352
+ 'proxy-authenticate',
353
+ 'proxy-authentication-info',
354
+ 'proxy-authorization',
355
+ 'proxy-connection',
356
+ 'te',
357
+ 'transfer-encoding',
358
+ 'upgrade',
359
+ // We'll add age back when serving it
360
+ 'age'
361
+ ]
362
+
363
+ if (headers['connection']) {
364
+ if (Array.isArray(headers['connection'])) {
365
+ // connection: a
366
+ // connection: b
367
+ headersToRemove.push(...headers['connection'].map(header => header.trim()))
368
+ } else {
369
+ // connection: a, b
370
+ headersToRemove.push(...headers['connection'].split(',').map(header => header.trim()))
371
+ }
372
+ }
342
373
 
343
374
  if (Array.isArray(cacheControlDirectives['no-cache'])) {
344
375
  headersToRemove.push(...cacheControlDirectives['no-cache'])
@@ -349,51 +380,14 @@ function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirect
349
380
  }
350
381
 
351
382
  let strippedHeaders
352
-
353
- let offset = 0
354
- for (let i = 0; i < parsedRawHeaders.length; i += 2) {
355
- const headerName = parsedRawHeaders[i]
356
-
357
- if (headersToRemove.includes(headerName)) {
358
- // We have at least one header we want to remove
359
- if (!strippedHeaders) {
360
- // This is the first header we want to remove, let's create the array
361
- // Since we're stripping headers, this will over allocate. We'll trim
362
- // it later.
363
- strippedHeaders = new Array(parsedRawHeaders.length)
364
-
365
- // Backfill the previous headers into it
366
- for (let j = 0; j < i; j += 2) {
367
- strippedHeaders[j] = parsedRawHeaders[j]
368
- strippedHeaders[j + 1] = parsedRawHeaders[j + 1]
369
- }
370
- }
371
-
372
- // We can't map indices 1:1 from stripped headers to rawHeaders without
373
- // creating holes (if we skip a header, we now have two holes where at
374
- // element should be). So, let's keep an offset to keep strippedHeaders
375
- // flattened. We can also use this at the end for trimming the empty
376
- // elements off of strippedHeaders.
377
- offset += 2
378
-
379
- continue
380
- }
381
-
382
- // We want to keep this header. Let's add it to strippedHeaders if it exists
383
- if (strippedHeaders) {
384
- strippedHeaders[i - offset] = parsedRawHeaders[i]
385
- strippedHeaders[i + 1 - offset] = parsedRawHeaders[i + 1]
383
+ for (const headerName of headersToRemove) {
384
+ if (headers[headerName]) {
385
+ strippedHeaders ??= { ...headers }
386
+ delete strippedHeaders[headerName]
386
387
  }
387
388
  }
388
389
 
389
- if (strippedHeaders) {
390
- // Trim off the empty values at the end
391
- strippedHeaders.length -= offset
392
- }
393
-
394
- return strippedHeaders
395
- ? util.encodeRawHeaders(strippedHeaders)
396
- : rawHeaders
390
+ return strippedHeaders ?? headers
397
391
  }
398
392
 
399
393
  module.exports = CacheHandler