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.
- package/docs/docs/api/Dispatcher.md +15 -189
- package/index.js +1 -0
- package/lib/cache/memory-cache-store.js +2 -0
- package/lib/cache/sqlite-cache-store.js +32 -43
- package/lib/core/errors.js +2 -2
- package/lib/dispatcher/dispatcher-base.js +4 -2
- package/lib/handler/cache-handler.js +148 -49
- package/lib/handler/cache-revalidation-handler.js +21 -10
- package/lib/handler/decorator-handler.js +3 -0
- package/lib/handler/redirect-handler.js +15 -38
- package/lib/handler/retry-handler.js +65 -100
- package/lib/handler/unwrap-handler.js +2 -2
- package/lib/handler/wrap-handler.js +2 -2
- package/lib/interceptor/cache.js +250 -201
- package/lib/interceptor/response-error.js +9 -5
- package/lib/util/cache.js +42 -31
- package/package.json +4 -3
- package/types/cache-interceptor.d.ts +40 -0
- package/types/dispatcher.d.ts +1 -1
package/lib/interceptor/cache.js
CHANGED
|
@@ -10,45 +10,15 @@ const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHea
|
|
|
10
10
|
const { AbortError } = require('../core/errors.js')
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @
|
|
13
|
+
* @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
|
|
14
14
|
*/
|
|
15
|
-
function sendGatewayTimeout (handler) {
|
|
16
|
-
let aborted = false
|
|
17
|
-
try {
|
|
18
|
-
if (typeof handler.onConnect === 'function') {
|
|
19
|
-
handler.onConnect(() => {
|
|
20
|
-
aborted = true
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
if (aborted) {
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (typeof handler.onHeaders === 'function') {
|
|
29
|
-
handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
|
|
30
|
-
if (aborted) {
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (typeof handler.onComplete === 'function') {
|
|
36
|
-
handler.onComplete([])
|
|
37
|
-
}
|
|
38
|
-
} catch (err) {
|
|
39
|
-
if (typeof handler.onError === 'function') {
|
|
40
|
-
handler.onError(err)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
15
|
|
|
45
16
|
/**
|
|
46
17
|
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
47
|
-
* @param {
|
|
48
|
-
* @param {import('../util/cache.js').CacheControlDirectives | undefined} cacheControlDirectives
|
|
18
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
|
|
49
19
|
* @returns {boolean}
|
|
50
20
|
*/
|
|
51
|
-
function needsRevalidation (result,
|
|
21
|
+
function needsRevalidation (result, cacheControlDirectives) {
|
|
52
22
|
if (cacheControlDirectives?.['no-cache']) {
|
|
53
23
|
// Always revalidate requests with the no-cache directive
|
|
54
24
|
return true
|
|
@@ -81,6 +51,219 @@ function needsRevalidation (result, age, cacheControlDirectives) {
|
|
|
81
51
|
return false
|
|
82
52
|
}
|
|
83
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @param {DispatchFn} dispatch
|
|
56
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
|
|
57
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
58
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
|
|
59
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
|
|
60
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
|
|
61
|
+
*/
|
|
62
|
+
function handleUncachedResponse (
|
|
63
|
+
dispatch,
|
|
64
|
+
globalOpts,
|
|
65
|
+
cacheKey,
|
|
66
|
+
handler,
|
|
67
|
+
opts,
|
|
68
|
+
reqCacheControl
|
|
69
|
+
) {
|
|
70
|
+
if (reqCacheControl?.['only-if-cached']) {
|
|
71
|
+
let aborted = false
|
|
72
|
+
try {
|
|
73
|
+
if (typeof handler.onConnect === 'function') {
|
|
74
|
+
handler.onConnect(() => {
|
|
75
|
+
aborted = true
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (aborted) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof handler.onHeaders === 'function') {
|
|
84
|
+
handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
|
|
85
|
+
if (aborted) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof handler.onComplete === 'function') {
|
|
91
|
+
handler.onComplete([])
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (typeof handler.onError === 'function') {
|
|
95
|
+
handler.onError(err)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
107
|
+
* @param {number} age
|
|
108
|
+
*/
|
|
109
|
+
function sendCachedValue (handler, opts, result, age, context) {
|
|
110
|
+
// TODO (perf): Readable.from path can be optimized...
|
|
111
|
+
const stream = util.isStream(result.body)
|
|
112
|
+
? result.body
|
|
113
|
+
: Readable.from(result.body ?? [])
|
|
114
|
+
|
|
115
|
+
assert(!stream.destroyed, 'stream should not be destroyed')
|
|
116
|
+
assert(!stream.readableDidRead, 'stream should not be readableDidRead')
|
|
117
|
+
|
|
118
|
+
const controller = {
|
|
119
|
+
resume () {
|
|
120
|
+
stream.resume()
|
|
121
|
+
},
|
|
122
|
+
pause () {
|
|
123
|
+
stream.pause()
|
|
124
|
+
},
|
|
125
|
+
get paused () {
|
|
126
|
+
return stream.isPaused()
|
|
127
|
+
},
|
|
128
|
+
get aborted () {
|
|
129
|
+
return stream.destroyed
|
|
130
|
+
},
|
|
131
|
+
get reason () {
|
|
132
|
+
return stream.errored
|
|
133
|
+
},
|
|
134
|
+
abort (reason) {
|
|
135
|
+
stream.destroy(reason ?? new AbortError())
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
stream
|
|
140
|
+
.on('error', function (err) {
|
|
141
|
+
if (!this.readableEnded) {
|
|
142
|
+
if (typeof handler.onResponseError === 'function') {
|
|
143
|
+
handler.onResponseError(controller, err)
|
|
144
|
+
} else {
|
|
145
|
+
throw err
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
.on('close', function () {
|
|
150
|
+
if (!this.errored) {
|
|
151
|
+
handler.onResponseEnd?.(controller, {})
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
handler.onRequestStart?.(controller, context)
|
|
156
|
+
|
|
157
|
+
if (stream.destroyed) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add the age header
|
|
162
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
|
|
163
|
+
// TODO (fix): What if headers.age already exists?
|
|
164
|
+
const headers = age != null ? { ...result.headers, age: String(age) } : result.headers
|
|
165
|
+
|
|
166
|
+
handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
|
|
167
|
+
|
|
168
|
+
if (opts.method === 'HEAD') {
|
|
169
|
+
stream.destroy()
|
|
170
|
+
} else {
|
|
171
|
+
stream.on('data', function (chunk) {
|
|
172
|
+
handler.onResponseData?.(controller, chunk)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {DispatchFn} dispatch
|
|
179
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
|
|
180
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
181
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
|
|
182
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
|
|
183
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
|
|
184
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result
|
|
185
|
+
*/
|
|
186
|
+
function handleResult (
|
|
187
|
+
dispatch,
|
|
188
|
+
globalOpts,
|
|
189
|
+
cacheKey,
|
|
190
|
+
handler,
|
|
191
|
+
opts,
|
|
192
|
+
reqCacheControl,
|
|
193
|
+
result
|
|
194
|
+
) {
|
|
195
|
+
if (!result) {
|
|
196
|
+
return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const now = Date.now()
|
|
200
|
+
if (now > result.deleteAt) {
|
|
201
|
+
// Response is expired, cache store shouldn't have given this to us
|
|
202
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const age = Math.round((now - result.cachedAt) / 1000)
|
|
206
|
+
if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
|
|
207
|
+
// Response is considered expired for this specific request
|
|
208
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
|
|
209
|
+
return dispatch(opts, handler)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check if the response is stale
|
|
213
|
+
if (needsRevalidation(result, reqCacheControl)) {
|
|
214
|
+
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
|
|
215
|
+
// If body is is stream we can't revalidate...
|
|
216
|
+
// TODO (fix): This could be less strict...
|
|
217
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let withinStaleIfErrorThreshold = false
|
|
221
|
+
const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
|
|
222
|
+
if (staleIfErrorExpiry) {
|
|
223
|
+
withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let headers = {
|
|
227
|
+
...opts.headers,
|
|
228
|
+
'if-modified-since': new Date(result.cachedAt).toUTCString(),
|
|
229
|
+
'if-none-match': result.etag
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (result.vary) {
|
|
233
|
+
headers = {
|
|
234
|
+
...headers,
|
|
235
|
+
...result.vary
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// We need to revalidate the response
|
|
240
|
+
return dispatch(
|
|
241
|
+
{
|
|
242
|
+
...opts,
|
|
243
|
+
headers
|
|
244
|
+
},
|
|
245
|
+
new CacheRevalidationHandler(
|
|
246
|
+
(success, context) => {
|
|
247
|
+
if (success) {
|
|
248
|
+
sendCachedValue(handler, opts, result, age, context)
|
|
249
|
+
} else if (util.isStream(result.body)) {
|
|
250
|
+
result.body.on('error', () => {}).destroy()
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
new CacheHandler(globalOpts, cacheKey, handler),
|
|
254
|
+
withinStaleIfErrorThreshold
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Dump request body.
|
|
260
|
+
if (util.isStream(opts.body)) {
|
|
261
|
+
opts.body.on('error', () => {}).destroy()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
sendCachedValue(handler, opts, result, age, null)
|
|
265
|
+
}
|
|
266
|
+
|
|
84
267
|
/**
|
|
85
268
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
|
|
86
269
|
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
|
|
@@ -88,7 +271,9 @@ function needsRevalidation (result, age, cacheControlDirectives) {
|
|
|
88
271
|
module.exports = (opts = {}) => {
|
|
89
272
|
const {
|
|
90
273
|
store = new MemoryCacheStore(),
|
|
91
|
-
methods = ['GET']
|
|
274
|
+
methods = ['GET'],
|
|
275
|
+
cacheByDefault = undefined,
|
|
276
|
+
type = 'shared'
|
|
92
277
|
} = opts
|
|
93
278
|
|
|
94
279
|
if (typeof opts !== 'object' || opts === null) {
|
|
@@ -98,28 +283,35 @@ module.exports = (opts = {}) => {
|
|
|
98
283
|
assertCacheStore(store, 'opts.store')
|
|
99
284
|
assertCacheMethods(methods, 'opts.methods')
|
|
100
285
|
|
|
286
|
+
if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
|
|
287
|
+
throw new TypeError(`exepcted opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
|
|
291
|
+
throw new TypeError(`exepcted opts.type to be shared, private, or undefined, got ${typeof type}`)
|
|
292
|
+
}
|
|
293
|
+
|
|
101
294
|
const globalOpts = {
|
|
102
295
|
store,
|
|
103
|
-
methods
|
|
296
|
+
methods,
|
|
297
|
+
cacheByDefault,
|
|
298
|
+
type
|
|
104
299
|
}
|
|
105
300
|
|
|
106
301
|
const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
|
|
107
302
|
|
|
108
303
|
return dispatch => {
|
|
109
304
|
return (opts, handler) => {
|
|
110
|
-
// TODO (fix): What if e.g. opts.headers has if-modified-since header? Or other headers
|
|
111
|
-
// that make things ambigious?
|
|
112
|
-
|
|
113
305
|
if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
|
|
114
306
|
// Not a method we want to cache or we don't have the origin, skip
|
|
115
307
|
return dispatch(opts, handler)
|
|
116
308
|
}
|
|
117
309
|
|
|
118
|
-
const
|
|
310
|
+
const reqCacheControl = opts.headers?.['cache-control']
|
|
119
311
|
? parseCacheControlHeader(opts.headers['cache-control'])
|
|
120
312
|
: undefined
|
|
121
313
|
|
|
122
|
-
if (
|
|
314
|
+
if (reqCacheControl?.['no-store']) {
|
|
123
315
|
return dispatch(opts, handler)
|
|
124
316
|
}
|
|
125
317
|
|
|
@@ -127,172 +319,29 @@ module.exports = (opts = {}) => {
|
|
|
127
319
|
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
128
320
|
*/
|
|
129
321
|
const cacheKey = makeCacheKey(opts)
|
|
130
|
-
|
|
131
|
-
// TODO (perf): For small entries support returning a Buffer instead of a stream.
|
|
132
|
-
// Maybe store should return { staleAt, headers, body, etc... } instead of a stream + stream.value?
|
|
133
|
-
// Where body can be a Buffer, string, stream or blob?
|
|
134
322
|
const result = store.get(cacheKey)
|
|
135
|
-
if (!result) {
|
|
136
|
-
if (requestCacheControl?.['only-if-cached']) {
|
|
137
|
-
// We only want cached responses
|
|
138
|
-
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
|
|
139
|
-
sendGatewayTimeout(handler)
|
|
140
|
-
return true
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
148
|
-
* @param {number} age
|
|
149
|
-
*/
|
|
150
|
-
const respondWithCachedValue = ({ headers, statusCode, statusMessage, body }, age, context) => {
|
|
151
|
-
const stream = util.isStream(body)
|
|
152
|
-
? body
|
|
153
|
-
: Readable.from(body ?? [])
|
|
154
|
-
|
|
155
|
-
assert(!stream.destroyed, 'stream should not be destroyed')
|
|
156
|
-
assert(!stream.readableDidRead, 'stream should not be readableDidRead')
|
|
157
|
-
|
|
158
|
-
const controller = {
|
|
159
|
-
resume () {
|
|
160
|
-
stream.resume()
|
|
161
|
-
},
|
|
162
|
-
pause () {
|
|
163
|
-
stream.pause()
|
|
164
|
-
},
|
|
165
|
-
get paused () {
|
|
166
|
-
return stream.isPaused()
|
|
167
|
-
},
|
|
168
|
-
get aborted () {
|
|
169
|
-
return stream.destroyed
|
|
170
|
-
},
|
|
171
|
-
get reason () {
|
|
172
|
-
return stream.errored
|
|
173
|
-
},
|
|
174
|
-
abort (reason) {
|
|
175
|
-
stream.destroy(reason ?? new AbortError())
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
stream
|
|
180
|
-
.on('error', function (err) {
|
|
181
|
-
if (!this.readableEnded) {
|
|
182
|
-
if (typeof handler.onResponseError === 'function') {
|
|
183
|
-
handler.onResponseError(controller, err)
|
|
184
|
-
} else {
|
|
185
|
-
throw err
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
.on('close', function () {
|
|
190
|
-
if (!this.errored) {
|
|
191
|
-
handler.onResponseEnd?.(controller, {})
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
handler.onRequestStart?.(controller, context)
|
|
196
|
-
|
|
197
|
-
if (stream.destroyed) {
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Add the age header
|
|
202
|
-
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
|
|
203
|
-
// TODO (fix): What if headers.age already exists?
|
|
204
|
-
headers = age != null ? { ...headers, age: String(age) } : headers
|
|
205
|
-
|
|
206
|
-
handler.onResponseStart?.(controller, statusCode, statusMessage, headers)
|
|
207
|
-
|
|
208
|
-
if (opts.method === 'HEAD') {
|
|
209
|
-
stream.destroy()
|
|
210
|
-
} else {
|
|
211
|
-
stream.on('data', function (chunk) {
|
|
212
|
-
handler.onResponseData?.(controller, chunk)
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
219
|
-
*/
|
|
220
|
-
const handleResult = (result) => {
|
|
221
|
-
// TODO (perf): Readable.from path can be optimized...
|
|
222
|
-
|
|
223
|
-
if (!result.body && opts.method !== 'HEAD') {
|
|
224
|
-
throw new Error('stream is undefined but method isn\'t HEAD')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const age = Math.round((Date.now() - result.cachedAt) / 1000)
|
|
228
|
-
if (requestCacheControl?.['max-age'] && age >= requestCacheControl['max-age']) {
|
|
229
|
-
// Response is considered expired for this specific request
|
|
230
|
-
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
|
|
231
|
-
return dispatch(opts, handler)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Check if the response is stale
|
|
235
|
-
if (needsRevalidation(result, age, requestCacheControl)) {
|
|
236
|
-
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
|
|
237
|
-
// If body is is stream we can't revalidate...
|
|
238
|
-
// TODO (fix): This could be less strict...
|
|
239
|
-
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
240
|
-
}
|
|
241
323
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
},
|
|
252
|
-
new CacheRevalidationHandler(
|
|
253
|
-
(success, context) => {
|
|
254
|
-
if (success) {
|
|
255
|
-
respondWithCachedValue(result, age, context)
|
|
256
|
-
} else if (util.isStream(result.body)) {
|
|
257
|
-
result.body.on('error', () => {}).destroy()
|
|
258
|
-
}
|
|
259
|
-
},
|
|
260
|
-
new CacheHandler(globalOpts, cacheKey, handler)
|
|
261
|
-
)
|
|
324
|
+
if (result && typeof result.then === 'function') {
|
|
325
|
+
result.then(result => {
|
|
326
|
+
handleResult(dispatch,
|
|
327
|
+
globalOpts,
|
|
328
|
+
cacheKey,
|
|
329
|
+
handler,
|
|
330
|
+
opts,
|
|
331
|
+
reqCacheControl,
|
|
332
|
+
result
|
|
262
333
|
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Dump request body.
|
|
266
|
-
if (util.isStream(opts.body)) {
|
|
267
|
-
opts.body.on('error', () => {}).destroy()
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
respondWithCachedValue(result, age, null)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (typeof result.then === 'function') {
|
|
274
|
-
result.then((result) => {
|
|
275
|
-
if (!result) {
|
|
276
|
-
if (requestCacheControl?.['only-if-cached']) {
|
|
277
|
-
// We only want cached responses
|
|
278
|
-
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
|
|
279
|
-
sendGatewayTimeout(handler)
|
|
280
|
-
return true
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
284
|
-
} else {
|
|
285
|
-
handleResult(result)
|
|
286
|
-
}
|
|
287
|
-
}, err => {
|
|
288
|
-
if (typeof handler.onError === 'function') {
|
|
289
|
-
handler.onError(err)
|
|
290
|
-
} else {
|
|
291
|
-
throw err
|
|
292
|
-
}
|
|
293
334
|
})
|
|
294
335
|
} else {
|
|
295
|
-
handleResult(
|
|
336
|
+
handleResult(
|
|
337
|
+
dispatch,
|
|
338
|
+
globalOpts,
|
|
339
|
+
cacheKey,
|
|
340
|
+
handler,
|
|
341
|
+
opts,
|
|
342
|
+
reqCacheControl,
|
|
343
|
+
result
|
|
344
|
+
)
|
|
296
345
|
}
|
|
297
346
|
|
|
298
347
|
return true
|
|
@@ -4,7 +4,7 @@ const { parseHeaders } = require('../core/util')
|
|
|
4
4
|
const DecoratorHandler = require('../handler/decorator-handler')
|
|
5
5
|
const { ResponseError } = require('../core/errors')
|
|
6
6
|
|
|
7
|
-
class
|
|
7
|
+
class ResponseErrorHandler extends DecoratorHandler {
|
|
8
8
|
#handler
|
|
9
9
|
#statusCode
|
|
10
10
|
#contentType
|
|
@@ -66,7 +66,7 @@ class Handler extends DecoratorHandler {
|
|
|
66
66
|
Error.stackTraceLimit = 0
|
|
67
67
|
try {
|
|
68
68
|
err = new ResponseError('Response Error', this.#statusCode, {
|
|
69
|
-
|
|
69
|
+
body: this.#body,
|
|
70
70
|
headers: this.#headers
|
|
71
71
|
})
|
|
72
72
|
} finally {
|
|
@@ -84,6 +84,10 @@ class Handler extends DecoratorHandler {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
module.exports = (
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
module.exports = () => {
|
|
88
|
+
return (dispatch) => {
|
|
89
|
+
return function Intercept (opts, handler) {
|
|
90
|
+
return dispatch(opts, new ResponseErrorHandler(opts, { handler }))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/lib/util/cache.js
CHANGED
|
@@ -96,36 +96,27 @@ function assertCacheValue (value) {
|
|
|
96
96
|
/**
|
|
97
97
|
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control
|
|
98
98
|
* @see https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
|
|
99
|
-
|
|
100
|
-
* @typedef {{
|
|
101
|
-
* 'max-stale'?: number;
|
|
102
|
-
* 'min-fresh'?: number;
|
|
103
|
-
* 'max-age'?: number;
|
|
104
|
-
* 's-maxage'?: number;
|
|
105
|
-
* 'stale-while-revalidate'?: number;
|
|
106
|
-
* 'stale-if-error'?: number;
|
|
107
|
-
* public?: true;
|
|
108
|
-
* private?: true | string[];
|
|
109
|
-
* 'no-store'?: true;
|
|
110
|
-
* 'no-cache'?: true | string[];
|
|
111
|
-
* 'must-revalidate'?: true;
|
|
112
|
-
* 'proxy-revalidate'?: true;
|
|
113
|
-
* immutable?: true;
|
|
114
|
-
* 'no-transform'?: true;
|
|
115
|
-
* 'must-understand'?: true;
|
|
116
|
-
* 'only-if-cached'?: true;
|
|
117
|
-
* }} CacheControlDirectives
|
|
118
|
-
*
|
|
99
|
+
|
|
119
100
|
* @param {string | string[]} header
|
|
120
|
-
* @returns {CacheControlDirectives}
|
|
101
|
+
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
|
|
121
102
|
*/
|
|
122
103
|
function parseCacheControlHeader (header) {
|
|
123
104
|
/**
|
|
124
|
-
* @type {import('
|
|
105
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
|
|
125
106
|
*/
|
|
126
107
|
const output = {}
|
|
127
108
|
|
|
128
|
-
|
|
109
|
+
let directives
|
|
110
|
+
if (Array.isArray(header)) {
|
|
111
|
+
directives = []
|
|
112
|
+
|
|
113
|
+
for (const directive of header) {
|
|
114
|
+
directives.push(...directive.split(','))
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
directives = header.split(',')
|
|
118
|
+
}
|
|
119
|
+
|
|
129
120
|
for (let i = 0; i < directives.length; i++) {
|
|
130
121
|
const directive = directives[i].toLowerCase()
|
|
131
122
|
const keyValueDelimiter = directive.indexOf('=')
|
|
@@ -133,10 +124,8 @@ function parseCacheControlHeader (header) {
|
|
|
133
124
|
let key
|
|
134
125
|
let value
|
|
135
126
|
if (keyValueDelimiter !== -1) {
|
|
136
|
-
key = directive.substring(0, keyValueDelimiter).
|
|
137
|
-
value = directive
|
|
138
|
-
.substring(keyValueDelimiter + 1)
|
|
139
|
-
.trim()
|
|
127
|
+
key = directive.substring(0, keyValueDelimiter).trimStart()
|
|
128
|
+
value = directive.substring(keyValueDelimiter + 1)
|
|
140
129
|
} else {
|
|
141
130
|
key = directive.trim()
|
|
142
131
|
}
|
|
@@ -148,16 +137,28 @@ function parseCacheControlHeader (header) {
|
|
|
148
137
|
case 's-maxage':
|
|
149
138
|
case 'stale-while-revalidate':
|
|
150
139
|
case 'stale-if-error': {
|
|
151
|
-
if (value === undefined) {
|
|
140
|
+
if (value === undefined || value[0] === ' ') {
|
|
152
141
|
continue
|
|
153
142
|
}
|
|
154
143
|
|
|
144
|
+
if (
|
|
145
|
+
value.length >= 2 &&
|
|
146
|
+
value[0] === '"' &&
|
|
147
|
+
value[value.length - 1] === '"'
|
|
148
|
+
) {
|
|
149
|
+
value = value.substring(1, value.length - 1)
|
|
150
|
+
}
|
|
151
|
+
|
|
155
152
|
const parsedValue = parseInt(value, 10)
|
|
156
153
|
// eslint-disable-next-line no-self-compare
|
|
157
154
|
if (parsedValue !== parsedValue) {
|
|
158
155
|
continue
|
|
159
156
|
}
|
|
160
157
|
|
|
158
|
+
if (key === 'max-age' && key in output && output[key] >= parsedValue) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
|
|
161
162
|
output[key] = parsedValue
|
|
162
163
|
|
|
163
164
|
break
|
|
@@ -206,11 +207,19 @@ function parseCacheControlHeader (header) {
|
|
|
206
207
|
headers[headers.length - 1] = lastHeader
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
|
|
210
|
+
if (key in output) {
|
|
211
|
+
output[key] = output[key].concat(headers)
|
|
212
|
+
} else {
|
|
213
|
+
output[key] = headers
|
|
214
|
+
}
|
|
210
215
|
}
|
|
211
216
|
} else {
|
|
212
217
|
// Something like `no-cache=some-header`
|
|
213
|
-
|
|
218
|
+
if (key in output) {
|
|
219
|
+
output[key] = output[key].concat(value)
|
|
220
|
+
} else {
|
|
221
|
+
output[key] = [value]
|
|
222
|
+
}
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
break
|
|
@@ -248,7 +257,7 @@ function parseCacheControlHeader (header) {
|
|
|
248
257
|
* @returns {Record<string, string | string[]>}
|
|
249
258
|
*/
|
|
250
259
|
function parseVaryHeader (varyHeader, headers) {
|
|
251
|
-
if (typeof varyHeader === 'string' && varyHeader
|
|
260
|
+
if (typeof varyHeader === 'string' && varyHeader.includes('*')) {
|
|
252
261
|
return headers
|
|
253
262
|
}
|
|
254
263
|
|
|
@@ -262,6 +271,8 @@ function parseVaryHeader (varyHeader, headers) {
|
|
|
262
271
|
|
|
263
272
|
if (headers[trimmedHeader]) {
|
|
264
273
|
output[trimmedHeader] = headers[trimmedHeader]
|
|
274
|
+
} else {
|
|
275
|
+
return undefined
|
|
265
276
|
}
|
|
266
277
|
}
|
|
267
278
|
|