undici 7.0.0-alpha.7 → 7.0.0-alpha.9

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.
@@ -10,7 +10,9 @@ const {
10
10
  function noop () {}
11
11
 
12
12
  /**
13
- * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
13
+ * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
14
+ *
15
+ * @implements {DispatchHandler}
14
16
  */
15
17
  class CacheHandler {
16
18
  /**
@@ -18,6 +20,16 @@ class CacheHandler {
18
20
  */
19
21
  #cacheKey
20
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
+
21
33
  /**
22
34
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
23
35
  */
@@ -38,10 +50,10 @@ class CacheHandler {
38
50
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
39
51
  * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
40
52
  */
41
- constructor (opts, cacheKey, handler) {
42
- const { store } = opts
43
-
53
+ constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
44
54
  this.#store = store
55
+ this.#cacheType = type
56
+ this.#cacheByDefault = cacheByDefault
45
57
  this.#cacheKey = cacheKey
46
58
  this.#handler = handler
47
59
  }
@@ -59,15 +71,15 @@ class CacheHandler {
59
71
  onResponseStart (
60
72
  controller,
61
73
  statusCode,
62
- statusMessage,
63
- headers
74
+ headers,
75
+ statusMessage
64
76
  ) {
65
77
  const downstreamOnHeaders = () =>
66
78
  this.#handler.onResponseStart?.(
67
79
  controller,
68
80
  statusCode,
69
- statusMessage,
70
- headers
81
+ headers,
82
+ statusMessage
71
83
  )
72
84
 
73
85
  if (
@@ -85,24 +97,47 @@ class CacheHandler {
85
97
  }
86
98
 
87
99
  const cacheControlHeader = headers['cache-control']
88
- if (!cacheControlHeader) {
100
+ if (!cacheControlHeader && !headers['expires'] && !this.#cacheByDefault) {
89
101
  // Don't have the cache control header or the cache is full
90
102
  return downstreamOnHeaders()
91
103
  }
92
104
 
93
- const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
94
- if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
105
+ const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
106
+ if (!canCacheResponse(this.#cacheType, statusCode, headers, cacheControlDirectives)) {
95
107
  return downstreamOnHeaders()
96
108
  }
97
109
 
110
+ const age = getAge(headers)
111
+
98
112
  const now = Date.now()
99
- const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
113
+ const staleAt = determineStaleAt(this.#cacheType, now, headers, cacheControlDirectives) ?? this.#cacheByDefault
100
114
  if (staleAt) {
101
- const varyDirectives = this.#cacheKey.headers && headers.vary
102
- ? parseVaryHeader(headers.vary, this.#cacheKey.headers)
103
- : undefined
104
- const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
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
105
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)
106
141
  const strippedHeaders = stripNecessaryHeaders(headers, cacheControlDirectives)
107
142
 
108
143
  /**
@@ -113,8 +148,9 @@ class CacheHandler {
113
148
  statusMessage,
114
149
  headers: strippedHeaders,
115
150
  vary: varyDirectives,
116
- cachedAt: now,
117
- staleAt,
151
+ cacheControlDirectives,
152
+ cachedAt: age ? now - (age * 1000) : now,
153
+ staleAt: absoluteStaleAt,
118
154
  deleteAt
119
155
  }
120
156
 
@@ -130,6 +166,7 @@ class CacheHandler {
130
166
  .on('drain', () => controller.resume())
131
167
  .on('error', function () {
132
168
  // TODO (fix): Make error somehow observable?
169
+ handler.#writeStream = undefined
133
170
  })
134
171
  .on('close', function () {
135
172
  if (handler.#writeStream === this) {
@@ -168,25 +205,29 @@ class CacheHandler {
168
205
  /**
169
206
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
170
207
  *
208
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
171
209
  * @param {number} statusCode
172
210
  * @param {Record<string, string | string[]>} headers
173
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
211
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
174
212
  */
175
- function canCacheResponse (statusCode, headers, cacheControlDirectives) {
213
+ function canCacheResponse (cacheType, statusCode, headers, cacheControlDirectives) {
176
214
  if (statusCode !== 200 && statusCode !== 307) {
177
215
  return false
178
216
  }
179
217
 
180
218
  if (
181
- cacheControlDirectives.private === true ||
182
219
  cacheControlDirectives['no-cache'] === true ||
183
220
  cacheControlDirectives['no-store']
184
221
  ) {
185
222
  return false
186
223
  }
187
224
 
225
+ if (cacheType === 'shared' && cacheControlDirectives.private === true) {
226
+ return false
227
+ }
228
+
188
229
  // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
189
- if (headers.vary === '*') {
230
+ if (headers.vary?.includes('*')) {
190
231
  return false
191
232
  }
192
233
 
@@ -215,63 +256,120 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
215
256
  }
216
257
 
217
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
218
277
  * @param {number} now
219
278
  * @param {Record<string, string | string[]>} headers
220
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
279
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
221
280
  *
222
281
  * @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
223
282
  */
224
- function determineStaleAt (now, headers, cacheControlDirectives) {
225
- // Prioritize s-maxage since we're a shared cache
226
- // s-maxage > max-age > Expire
227
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
228
- const sMaxAge = cacheControlDirectives['s-maxage']
229
- if (sMaxAge) {
230
- return now + (sMaxAge * 1000)
231
- }
232
-
233
- if (cacheControlDirectives.immutable) {
234
- // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
235
- 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
+ }
236
292
  }
237
293
 
238
294
  const maxAge = cacheControlDirectives['max-age']
239
295
  if (maxAge) {
240
- return now + (maxAge * 1000)
296
+ return maxAge * 1000
241
297
  }
242
298
 
243
- if (headers.expire && typeof headers.expire === 'string') {
299
+ if (headers.expires && typeof headers.expires === 'string') {
244
300
  // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
245
- const expiresDate = new Date(headers.expire)
301
+ const expiresDate = new Date(headers.expires)
246
302
  if (expiresDate instanceof Date && Number.isFinite(expiresDate.valueOf())) {
247
- return now + (Date.now() - expiresDate.getTime())
303
+ if (now >= expiresDate.getTime()) {
304
+ return undefined
305
+ }
306
+
307
+ return expiresDate.getTime() - now
248
308
  }
249
309
  }
250
310
 
311
+ if (cacheControlDirectives.immutable) {
312
+ // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
313
+ return 31536000
314
+ }
315
+
251
316
  return undefined
252
317
  }
253
318
 
254
319
  /**
255
- * @param {number} now
256
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
320
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
257
321
  * @param {number} staleAt
258
322
  */
259
- function determineDeleteAt (now, cacheControlDirectives, staleAt) {
323
+ function determineDeleteAt (cacheControlDirectives, staleAt) {
324
+ let staleWhileRevalidate = -Infinity
325
+ let staleIfError = -Infinity
326
+ let immutable = -Infinity
327
+
260
328
  if (cacheControlDirectives['stale-while-revalidate']) {
261
- 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)
334
+ }
335
+
336
+ if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
337
+ immutable = 31536000
262
338
  }
263
339
 
264
- return staleAt
340
+ return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
265
341
  }
266
342
 
267
343
  /**
268
344
  * Strips headers required to be removed in cached responses
269
345
  * @param {Record<string, string | string[]>} headers
270
- * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
346
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
271
347
  * @returns {Record<string, string | string []>}
272
348
  */
273
349
  function stripNecessaryHeaders (headers, cacheControlDirectives) {
274
- const headersToRemove = ['connection']
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
+ }
275
373
 
276
374
  if (Array.isArray(cacheControlDirectives['no-cache'])) {
277
375
  headersToRemove.push(...cacheControlDirectives['no-cache'])
@@ -282,12 +380,13 @@ function stripNecessaryHeaders (headers, cacheControlDirectives) {
282
380
  }
283
381
 
284
382
  let strippedHeaders
285
- for (const headerName of Object.keys(headers)) {
286
- if (headersToRemove.includes(headerName)) {
383
+ for (const headerName of headersToRemove) {
384
+ if (headers[headerName]) {
287
385
  strippedHeaders ??= { ...headers }
288
- delete headers[headerName]
386
+ delete strippedHeaders[headerName]
289
387
  }
290
388
  }
389
+
291
390
  return strippedHeaders ?? headers
292
391
  }
293
392
 
@@ -17,10 +17,12 @@ const assert = require('node:assert')
17
17
  */
18
18
  class CacheRevalidationHandler {
19
19
  #successful = false
20
+
20
21
  /**
21
22
  * @type {((boolean, any) => void) | null}
22
23
  */
23
24
  #callback
25
+
24
26
  /**
25
27
  * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)}
26
28
  */
@@ -29,19 +31,26 @@ class CacheRevalidationHandler {
29
31
  #context
30
32
 
31
33
  /**
32
- * @param {(boolean, any) => void} callback Function to call if the cached value is valid
33
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
34
+ * @type {boolean}
35
+ */
36
+ #allowErrorStatusCodes
37
+
38
+ /**
39
+ * @param {(boolean) => void} callback Function to call if the cached value is valid
40
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
41
+ * @param {boolean} allowErrorStatusCodes
34
42
  */
35
- constructor (callback, handler) {
43
+ constructor (callback, handler, allowErrorStatusCodes) {
36
44
  if (typeof callback !== 'function') {
37
45
  throw new TypeError('callback must be a function')
38
46
  }
39
47
 
40
48
  this.#callback = callback
41
49
  this.#handler = handler
50
+ this.#allowErrorStatusCodes = allowErrorStatusCodes
42
51
  }
43
52
 
44
- onRequestStart (controller, context) {
53
+ onRequestStart (_, context) {
45
54
  this.#successful = false
46
55
  this.#context = context
47
56
  }
@@ -53,13 +62,15 @@ class CacheRevalidationHandler {
53
62
  onResponseStart (
54
63
  controller,
55
64
  statusCode,
56
- statusMessage,
57
- headers
65
+ headers,
66
+ statusMessage
58
67
  ) {
59
68
  assert(this.#callback != null)
60
69
 
61
70
  // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
62
- this.#successful = statusCode === 304
71
+ // https://datatracker.ietf.org/doc/html/rfc5861#section-4
72
+ this.#successful = statusCode === 304 ||
73
+ (this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504)
63
74
  this.#callback(this.#successful, this.#context)
64
75
  this.#callback = null
65
76
 
@@ -71,8 +82,8 @@ class CacheRevalidationHandler {
71
82
  this.#handler.onResponseStart?.(
72
83
  controller,
73
84
  statusCode,
74
- statusMessage,
75
- headers
85
+ headers,
86
+ statusMessage
76
87
  )
77
88
  }
78
89
 
@@ -81,7 +92,7 @@ class CacheRevalidationHandler {
81
92
  return
82
93
  }
83
94
 
84
- return this.#handler.onResponseData(controller, chunk)
95
+ return this.#handler.onResponseData?.(controller, chunk)
85
96
  }
86
97
 
87
98
  onResponseEnd (controller, trailers) {
@@ -2,6 +2,9 @@
2
2
 
3
3
  const assert = require('node:assert')
4
4
 
5
+ /**
6
+ * @deprecated
7
+ */
5
8
  module.exports = class DecoratorHandler {
6
9
  #handler
7
10
  #onCompleteCalled = false
@@ -42,7 +42,6 @@ class RedirectHandler {
42
42
 
43
43
  this.dispatch = dispatch
44
44
  this.location = null
45
- this.abort = null
46
45
  this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy
47
46
  this.maxRedirections = maxRedirections
48
47
  this.handler = handler
@@ -83,20 +82,15 @@ class RedirectHandler {
83
82
  }
84
83
  }
85
84
 
86
- onConnect (abort) {
87
- this.abort = abort
88
- this.handler.onConnect(abort, { history: this.history })
85
+ onRequestStart (controller, context) {
86
+ this.handler.onRequestStart?.(controller, { ...context, history: this.history })
89
87
  }
90
88
 
91
- onUpgrade (statusCode, headers, socket) {
92
- this.handler.onUpgrade(statusCode, headers, socket)
89
+ onRequestUpgrade (controller, statusCode, headers, socket) {
90
+ this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
93
91
  }
94
92
 
95
- onError (error) {
96
- this.handler.onError(error)
97
- }
98
-
99
- onHeaders (statusCode, rawHeaders, resume, statusText) {
93
+ onResponseStart (controller, statusCode, headers, statusMessage) {
100
94
  if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
101
95
  throw new Error('max redirects')
102
96
  }
@@ -122,16 +116,17 @@ class RedirectHandler {
122
116
  this.opts.body = null
123
117
  }
124
118
 
125
- this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
119
+ this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) || redirectableStatusCodes.indexOf(statusCode) === -1
126
120
  ? null
127
- : parseLocation(statusCode, rawHeaders)
121
+ : headers.location
128
122
 
129
123
  if (this.opts.origin) {
130
124
  this.history.push(new URL(this.opts.path, this.opts.origin))
131
125
  }
132
126
 
133
127
  if (!this.location) {
134
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText)
128
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
129
+ return
135
130
  }
136
131
 
137
132
  const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
@@ -147,7 +142,7 @@ class RedirectHandler {
147
142
  this.opts.query = null
148
143
  }
149
144
 
150
- onData (chunk) {
145
+ onResponseData (controller, chunk) {
151
146
  if (this.location) {
152
147
  /*
153
148
  https://tools.ietf.org/html/rfc7231#section-6.4
@@ -167,11 +162,11 @@ class RedirectHandler {
167
162
  servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
168
163
  */
169
164
  } else {
170
- return this.handler.onData(chunk)
165
+ this.handler.onResponseData?.(controller, chunk)
171
166
  }
172
167
  }
173
168
 
174
- onComplete (trailers) {
169
+ onResponseEnd (controller, trailers) {
175
170
  if (this.location) {
176
171
  /*
177
172
  https://tools.ietf.org/html/rfc7231#section-6.4
@@ -181,32 +176,14 @@ class RedirectHandler {
181
176
 
182
177
  See comment on onData method above for more detailed information.
183
178
  */
184
-
185
- this.location = null
186
- this.abort = null
187
-
188
179
  this.dispatch(this.opts, this)
189
180
  } else {
190
- this.handler.onComplete(trailers)
181
+ this.handler.onResponseEnd(controller, trailers)
191
182
  }
192
183
  }
193
184
 
194
- onBodySent (chunk) {
195
- if (this.handler.onBodySent) {
196
- this.handler.onBodySent(chunk)
197
- }
198
- }
199
- }
200
-
201
- function parseLocation (statusCode, rawHeaders) {
202
- if (redirectableStatusCodes.indexOf(statusCode) === -1) {
203
- return null
204
- }
205
-
206
- for (let i = 0; i < rawHeaders.length; i += 2) {
207
- if (rawHeaders[i].length === 8 && util.headerNameToString(rawHeaders[i]) === 'location') {
208
- return rawHeaders[i + 1]
209
- }
185
+ onResponseError (controller, error) {
186
+ this.handler.onResponseError?.(controller, error)
210
187
  }
211
188
  }
212
189