undici 7.0.0-alpha.2 → 7.0.0-alpha.3

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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/docs/docs/api/CacheStore.md +116 -0
  3. package/docs/docs/api/Dispatcher.md +10 -0
  4. package/index.js +6 -1
  5. package/lib/api/api-request.js +1 -1
  6. package/lib/api/readable.js +6 -6
  7. package/lib/cache/memory-cache-store.js +417 -0
  8. package/lib/core/constants.js +24 -1
  9. package/lib/core/util.js +41 -2
  10. package/lib/dispatcher/client-h1.js +100 -87
  11. package/lib/dispatcher/client-h2.js +127 -75
  12. package/lib/dispatcher/pool-base.js +3 -3
  13. package/lib/handler/cache-handler.js +359 -0
  14. package/lib/handler/cache-revalidation-handler.js +119 -0
  15. package/lib/interceptor/cache.js +171 -0
  16. package/lib/util/cache.js +224 -0
  17. package/lib/web/cache/cache.js +1 -0
  18. package/lib/web/cache/cachestorage.js +2 -0
  19. package/lib/web/eventsource/eventsource.js +2 -0
  20. package/lib/web/fetch/constants.js +12 -5
  21. package/lib/web/fetch/data-url.js +2 -2
  22. package/lib/web/fetch/formdata.js +3 -1
  23. package/lib/web/fetch/headers.js +2 -0
  24. package/lib/web/fetch/request.js +3 -1
  25. package/lib/web/fetch/response.js +3 -1
  26. package/lib/web/fetch/util.js +171 -47
  27. package/lib/web/fetch/webidl.js +16 -12
  28. package/lib/web/websocket/constants.js +67 -6
  29. package/lib/web/websocket/events.js +4 -0
  30. package/lib/web/websocket/stream/websocketerror.js +1 -1
  31. package/lib/web/websocket/websocket.js +2 -0
  32. package/package.json +7 -3
  33. package/types/cache-interceptor.d.ts +97 -0
  34. package/types/fetch.d.ts +9 -8
  35. package/types/index.d.ts +3 -0
  36. package/types/interceptors.d.ts +4 -0
  37. package/types/webidl.d.ts +7 -1
@@ -0,0 +1,359 @@
1
+ 'use strict'
2
+
3
+ const util = require('../core/util')
4
+ const DecoratorHandler = require('../handler/decorator-handler')
5
+ const {
6
+ parseCacheControlHeader,
7
+ parseVaryHeader
8
+ } = require('../util/cache')
9
+
10
+ /**
11
+ * Writes a response to a CacheStore and then passes it on to the next handler
12
+ */
13
+ class CacheHandler extends DecoratorHandler {
14
+ /**
15
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
16
+ */
17
+ #store
18
+
19
+ /**
20
+ * @type {import('../../types/dispatcher.d.ts').default.RequestOptions}
21
+ */
22
+ #requestOptions
23
+
24
+ /**
25
+ * @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
26
+ */
27
+ #handler
28
+
29
+ /**
30
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheStoreWriteable | undefined}
31
+ */
32
+ #writeStream
33
+
34
+ /**
35
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
36
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} requestOptions
37
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
38
+ */
39
+ constructor (opts, requestOptions, handler) {
40
+ const { store } = opts
41
+
42
+ super(handler)
43
+
44
+ this.#store = store
45
+ this.#requestOptions = requestOptions
46
+ this.#handler = handler
47
+ }
48
+
49
+ /**
50
+ * @see {DispatchHandlers.onHeaders}
51
+ *
52
+ * @param {number} statusCode
53
+ * @param {Buffer[]} rawHeaders
54
+ * @param {() => void} resume
55
+ * @param {string} statusMessage
56
+ * @returns {boolean}
57
+ */
58
+ onHeaders (
59
+ statusCode,
60
+ rawHeaders,
61
+ resume,
62
+ statusMessage
63
+ ) {
64
+ const downstreamOnHeaders = () => this.#handler.onHeaders(
65
+ statusCode,
66
+ rawHeaders,
67
+ resume,
68
+ statusMessage
69
+ )
70
+
71
+ if (
72
+ !util.safeHTTPMethods.includes(this.#requestOptions.method) &&
73
+ statusCode >= 200 &&
74
+ statusCode <= 399
75
+ ) {
76
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-respons
77
+ // Try/catch for if it's synchronous
78
+ try {
79
+ const result = this.#store.deleteByOrigin(this.#requestOptions.origin)
80
+ if (
81
+ result &&
82
+ typeof result.catch === 'function' &&
83
+ typeof this.#handler.onError === 'function'
84
+ ) {
85
+ // Fail silently
86
+ result.catch(_ => {})
87
+ }
88
+ } catch (err) {
89
+ // Fail silently
90
+ }
91
+
92
+ return downstreamOnHeaders()
93
+ }
94
+
95
+ const headers = util.parseHeaders(rawHeaders)
96
+
97
+ const cacheControlHeader = headers['cache-control']
98
+ const contentLengthHeader = headers['content-length']
99
+
100
+ if (!cacheControlHeader || !contentLengthHeader || this.#store.isFull) {
101
+ // Don't have the headers we need, can't cache
102
+ return downstreamOnHeaders()
103
+ }
104
+
105
+ const contentLength = Number(contentLengthHeader)
106
+ if (!Number.isInteger(contentLength)) {
107
+ return downstreamOnHeaders()
108
+ }
109
+
110
+ const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
111
+ if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
112
+ return downstreamOnHeaders()
113
+ }
114
+
115
+ const now = Date.now()
116
+ const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
117
+ if (staleAt) {
118
+ const varyDirectives = headers.vary
119
+ ? parseVaryHeader(headers.vary, this.#requestOptions.headers)
120
+ : undefined
121
+ const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
122
+
123
+ const strippedHeaders = stripNecessaryHeaders(
124
+ rawHeaders,
125
+ headers,
126
+ cacheControlDirectives
127
+ )
128
+
129
+ this.#writeStream = this.#store.createWriteStream(this.#requestOptions, {
130
+ statusCode,
131
+ statusMessage,
132
+ rawHeaders: strippedHeaders,
133
+ vary: varyDirectives,
134
+ cachedAt: now,
135
+ staleAt,
136
+ deleteAt
137
+ })
138
+
139
+ if (this.#writeStream) {
140
+ this.#writeStream.on('drain', resume)
141
+ this.#writeStream.on('error', () => {
142
+ this.#writeStream = undefined
143
+ resume()
144
+ })
145
+ }
146
+ }
147
+
148
+ if (typeof this.#handler.onHeaders === 'function') {
149
+ return downstreamOnHeaders()
150
+ }
151
+ return false
152
+ }
153
+
154
+ /**
155
+ * @see {DispatchHandlers.onData}
156
+ *
157
+ * @param {Buffer} chunk
158
+ * @returns {boolean}
159
+ */
160
+ onData (chunk) {
161
+ let paused = false
162
+
163
+ if (this.#writeStream) {
164
+ paused ||= this.#writeStream.write(chunk) === false
165
+ }
166
+
167
+ if (typeof this.#handler.onData === 'function') {
168
+ paused ||= this.#handler.onData(chunk) === false
169
+ }
170
+
171
+ return !paused
172
+ }
173
+
174
+ /**
175
+ * @see {DispatchHandlers.onComplete}
176
+ *
177
+ * @param {string[] | null} rawTrailers
178
+ */
179
+ onComplete (rawTrailers) {
180
+ if (this.#writeStream) {
181
+ if (rawTrailers) {
182
+ this.#writeStream.rawTrailers = rawTrailers
183
+ }
184
+
185
+ this.#writeStream.end()
186
+ }
187
+
188
+ if (typeof this.#handler.onComplete === 'function') {
189
+ return this.#handler.onComplete(rawTrailers)
190
+ }
191
+ }
192
+
193
+ /**
194
+ * @see {DispatchHandlers.onError}
195
+ *
196
+ * @param {Error} err
197
+ */
198
+ onError (err) {
199
+ if (this.#writeStream) {
200
+ this.#writeStream.destroy(err)
201
+ this.#writeStream = undefined
202
+ }
203
+
204
+ if (typeof this.#handler.onError === 'function') {
205
+ this.#handler.onError(err)
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
212
+ *
213
+ * @param {number} statusCode
214
+ * @param {Record<string, string>} headers
215
+ * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
216
+ */
217
+ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
218
+ if (
219
+ statusCode !== 200 &&
220
+ statusCode !== 307
221
+ ) {
222
+ return false
223
+ }
224
+
225
+ if (
226
+ !cacheControlDirectives.public ||
227
+ cacheControlDirectives.private === true ||
228
+ cacheControlDirectives['no-cache'] === true ||
229
+ cacheControlDirectives['no-store']
230
+ ) {
231
+ return false
232
+ }
233
+
234
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
235
+ if (headers.vary === '*') {
236
+ return false
237
+ }
238
+
239
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
240
+ if (headers['authorization']) {
241
+ if (
242
+ Array.isArray(cacheControlDirectives['no-cache']) &&
243
+ cacheControlDirectives['no-cache'].includes('authorization')
244
+ ) {
245
+ return false
246
+ }
247
+
248
+ if (
249
+ Array.isArray(cacheControlDirectives['private']) &&
250
+ cacheControlDirectives['private'].includes('authorization')
251
+ ) {
252
+ return false
253
+ }
254
+ }
255
+
256
+ return true
257
+ }
258
+
259
+ /**
260
+ * @param {number} now
261
+ * @param {Record<string, string | string[]>} headers
262
+ * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
263
+ *
264
+ * @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
265
+ */
266
+ function determineStaleAt (now, headers, cacheControlDirectives) {
267
+ // Prioritize s-maxage since we're a shared cache
268
+ // s-maxage > max-age > Expire
269
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
270
+ const sMaxAge = cacheControlDirectives['s-maxage']
271
+ if (sMaxAge) {
272
+ return now + (sMaxAge * 1000)
273
+ }
274
+
275
+ if (cacheControlDirectives.immutable) {
276
+ // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
277
+ return now + 31536000
278
+ }
279
+
280
+ const maxAge = cacheControlDirectives['max-age']
281
+ if (maxAge) {
282
+ return now + (maxAge * 1000)
283
+ }
284
+
285
+ if (headers.expire) {
286
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
287
+ return now + (Date.now() - new Date(headers.expire).getTime())
288
+ }
289
+
290
+ return undefined
291
+ }
292
+
293
+ /**
294
+ * @param {number} now
295
+ * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
296
+ * @param {number} staleAt
297
+ */
298
+ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
299
+ if (cacheControlDirectives['stale-while-revalidate']) {
300
+ return now + (cacheControlDirectives['stale-while-revalidate'] * 1000)
301
+ }
302
+
303
+ return staleAt
304
+ }
305
+
306
+ /**
307
+ * Strips headers required to be removed in cached responses
308
+ * @param {Buffer[]} rawHeaders
309
+ * @param {Record<string, string | string[]>} parsedHeaders
310
+ * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
311
+ * @returns {(Buffer|Buffer[])[]}
312
+ */
313
+ function stripNecessaryHeaders (rawHeaders, parsedHeaders, cacheControlDirectives) {
314
+ const headersToRemove = ['connection']
315
+
316
+ if (Array.isArray(cacheControlDirectives['no-cache'])) {
317
+ headersToRemove.push(...cacheControlDirectives['no-cache'])
318
+ }
319
+
320
+ if (Array.isArray(cacheControlDirectives['private'])) {
321
+ headersToRemove.push(...cacheControlDirectives['private'])
322
+ }
323
+
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
+ let strippedHeaders
330
+
331
+ const headerNames = Object.keys(parsedHeaders)
332
+ for (let i = 0; i < headerNames.length; i++) {
333
+ const header = headerNames[i]
334
+
335
+ if (headersToRemove.indexOf(header) !== -1) {
336
+ // We have a at least one header we want to remove
337
+ if (!strippedHeaders) {
338
+ // This is the first header we want to remove, let's create the object
339
+ // and backfill the previous headers into it
340
+ strippedHeaders = {}
341
+
342
+ for (let j = 0; j < i; j++) {
343
+ strippedHeaders[headerNames[j]] = parsedHeaders[headerNames[j]]
344
+ }
345
+ }
346
+
347
+ continue
348
+ }
349
+
350
+ // This header is fine. Let's add it to strippedHeaders if it exists.
351
+ if (strippedHeaders) {
352
+ strippedHeaders[header] = parsedHeaders[header]
353
+ }
354
+ }
355
+
356
+ return strippedHeaders ? util.encodeHeaders(strippedHeaders) : rawHeaders
357
+ }
358
+
359
+ module.exports = CacheHandler
@@ -0,0 +1,119 @@
1
+ 'use strict'
2
+
3
+ const DecoratorHandler = require('../handler/decorator-handler')
4
+
5
+ /**
6
+ * This takes care of revalidation requests we send to the origin. If we get
7
+ * a response indicating that what we have is cached (via a HTTP 304), we can
8
+ * continue using the cached value. Otherwise, we'll receive the new response
9
+ * here, which we then just pass on to the next handler (most likely a
10
+ * CacheHandler). Note that this assumes the proper headers were already
11
+ * included in the request to tell the origin that we want to revalidate the
12
+ * response (i.e. if-modified-since).
13
+ *
14
+ * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
15
+ *
16
+ * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers
17
+ * @implements {DispatchHandlers}
18
+ */
19
+ class CacheRevalidationHandler extends DecoratorHandler {
20
+ #successful = false
21
+ /**
22
+ * @type {(() => void)}
23
+ */
24
+ #successCallback
25
+ /**
26
+ * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)}
27
+ */
28
+ #handler
29
+
30
+ /**
31
+ * @param {() => void} successCallback Function to call if the cached value is valid
32
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
33
+ */
34
+ constructor (successCallback, handler) {
35
+ if (typeof successCallback !== 'function') {
36
+ throw new TypeError('successCallback must be a function')
37
+ }
38
+
39
+ super(handler)
40
+
41
+ this.#successCallback = successCallback
42
+ this.#handler = handler
43
+ }
44
+
45
+ /**
46
+ * @see {DispatchHandlers.onHeaders}
47
+ *
48
+ * @param {number} statusCode
49
+ * @param {Buffer[]} rawHeaders
50
+ * @param {() => void} resume
51
+ * @param {string} statusMessage
52
+ * @returns {boolean}
53
+ */
54
+ onHeaders (
55
+ statusCode,
56
+ rawHeaders,
57
+ resume,
58
+ statusMessage
59
+ ) {
60
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
61
+ if (statusCode === 304) {
62
+ this.#successful = true
63
+ this.#successCallback()
64
+ return true
65
+ }
66
+
67
+ if (typeof this.#handler.onHeaders === 'function') {
68
+ return this.#handler.onHeaders(
69
+ statusCode,
70
+ rawHeaders,
71
+ resume,
72
+ statusMessage
73
+ )
74
+ }
75
+ return false
76
+ }
77
+
78
+ /**
79
+ * @see {DispatchHandlers.onData}
80
+ *
81
+ * @param {Buffer} chunk
82
+ * @returns {boolean}
83
+ */
84
+ onData (chunk) {
85
+ if (this.#successful) {
86
+ return true
87
+ }
88
+
89
+ if (typeof this.#handler.onData === 'function') {
90
+ return this.#handler.onData(chunk)
91
+ }
92
+
93
+ return false
94
+ }
95
+
96
+ /**
97
+ * @see {DispatchHandlers.onComplete}
98
+ *
99
+ * @param {string[] | null} rawTrailers
100
+ */
101
+ onComplete (rawTrailers) {
102
+ if (!this.#successful && typeof this.#handler.onComplete === 'function') {
103
+ this.#handler.onComplete(rawTrailers)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * @see {DispatchHandlers.onError}
109
+ *
110
+ * @param {Error} err
111
+ */
112
+ onError (err) {
113
+ if (typeof this.#handler.onError === 'function') {
114
+ this.#handler.onError(err)
115
+ }
116
+ }
117
+ }
118
+
119
+ module.exports = CacheRevalidationHandler
@@ -0,0 +1,171 @@
1
+ 'use strict'
2
+
3
+ const util = require('../core/util')
4
+ const CacheHandler = require('../handler/cache-handler')
5
+ const MemoryCacheStore = require('../cache/memory-cache-store')
6
+ const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
7
+ const { assertCacheStore, assertCacheMethods } = require('../util/cache.js')
8
+
9
+ const AGE_HEADER = Buffer.from('age')
10
+
11
+ /**
12
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
13
+ * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
14
+ */
15
+ module.exports = (opts = {}) => {
16
+ const {
17
+ store = new MemoryCacheStore(),
18
+ methods = ['GET']
19
+ } = opts
20
+
21
+ if (typeof opts !== 'object' || opts === null) {
22
+ throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
23
+ }
24
+
25
+ assertCacheStore(store, 'opts.store')
26
+ assertCacheMethods(methods, 'opts.methods')
27
+
28
+ const globalOpts = {
29
+ store,
30
+ methods
31
+ }
32
+
33
+ const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
34
+
35
+ return dispatch => {
36
+ return (opts, handler) => {
37
+ if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
38
+ // Not a method we want to cache or we don't have the origin, skip
39
+ return dispatch(opts, handler)
40
+ }
41
+
42
+ const stream = store.createReadStream(opts)
43
+ if (!stream) {
44
+ // Request isn't cached
45
+ return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
46
+ }
47
+
48
+ let onErrorCalled = false
49
+
50
+ /**
51
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable} stream
52
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} value
53
+ */
54
+ const respondWithCachedValue = (stream, value) => {
55
+ const ac = new AbortController()
56
+ const signal = ac.signal
57
+
58
+ signal.onabort = (_, err) => {
59
+ stream.destroy()
60
+ if (!onErrorCalled) {
61
+ handler.onError(err)
62
+ onErrorCalled = true
63
+ }
64
+ }
65
+
66
+ stream.on('error', (err) => {
67
+ if (!onErrorCalled) {
68
+ handler.onError(err)
69
+ onErrorCalled = true
70
+ }
71
+ })
72
+
73
+ try {
74
+ if (typeof handler.onConnect === 'function') {
75
+ handler.onConnect(ac.abort)
76
+ signal.throwIfAborted()
77
+ }
78
+
79
+ if (typeof handler.onHeaders === 'function') {
80
+ // Add the age header
81
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
82
+ const age = Math.round((Date.now() - value.cachedAt) / 1000)
83
+
84
+ value.rawHeaders.push(AGE_HEADER, Buffer.from(`${age}`))
85
+
86
+ handler.onHeaders(value.statusCode, value.rawHeaders, stream.resume, value.statusMessage)
87
+ signal.throwIfAborted()
88
+ }
89
+
90
+ if (opts.method === 'HEAD') {
91
+ if (typeof handler.onComplete === 'function') {
92
+ handler.onComplete(null)
93
+ stream.destroy()
94
+ }
95
+ } else {
96
+ if (typeof handler.onData === 'function') {
97
+ stream.on('data', chunk => {
98
+ if (!handler.onData(chunk)) {
99
+ stream.pause()
100
+ }
101
+ })
102
+ }
103
+
104
+ if (typeof handler.onComplete === 'function') {
105
+ stream.on('end', () => {
106
+ handler.onComplete(value.rawTrailers ?? [])
107
+ })
108
+ }
109
+ }
110
+ } catch (err) {
111
+ stream.destroy(err)
112
+ if (!onErrorCalled && typeof handler.onError === 'function') {
113
+ handler.onError(err)
114
+ onErrorCalled = true
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable | undefined} stream
121
+ */
122
+ const handleStream = (stream) => {
123
+ if (!stream) {
124
+ // Request isn't cached
125
+ return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
126
+ }
127
+
128
+ const { value } = stream
129
+
130
+ // Dump body on error
131
+ if (util.isStream(opts.body)) {
132
+ opts.body?.on('error', () => {}).resume()
133
+ }
134
+
135
+ // Check if the response is stale
136
+ const now = Date.now()
137
+ if (now >= value.staleAt) {
138
+ if (now >= value.deleteAt) {
139
+ // Safety check in case the store gave us a response that should've been
140
+ // deleted already
141
+ dispatch(opts, new CacheHandler(globalOpts, opts, handler))
142
+ return
143
+ }
144
+
145
+ if (!opts.headers) {
146
+ opts.headers = {}
147
+ }
148
+
149
+ opts.headers['if-modified-since'] = new Date(value.cachedAt).toUTCString()
150
+
151
+ // Need to revalidate the response
152
+ dispatch(
153
+ opts,
154
+ new CacheRevalidationHandler(
155
+ () => respondWithCachedValue(stream, value),
156
+ new CacheHandler(globalOpts, opts, handler)
157
+ )
158
+ )
159
+
160
+ return
161
+ }
162
+
163
+ respondWithCachedValue(stream, value)
164
+ }
165
+
166
+ Promise.resolve(stream).then(handleStream).catch(handler.onError)
167
+
168
+ return true
169
+ }
170
+ }
171
+ }