@nxtedition/nxt-undici 7.3.18 → 7.3.19
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/lib/index.d.ts +0 -2
- package/lib/index.js +1 -1
- package/lib/interceptor/cache.js +67 -2
- package/lib/interceptor/log.js +3 -3
- package/lib/interceptor/response-error.js +2 -0
- package/lib/interceptor/response-retry.js +0 -9
- package/lib/interceptor/response-verify.js +26 -7
- package/lib/sqlite-cache-store.js +6 -14
- package/lib/utils.js +0 -8
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -155,7 +155,6 @@ export interface CacheValue {
|
|
|
155
155
|
etag?: string
|
|
156
156
|
vary?: Record<string, string | string[]>
|
|
157
157
|
cachedAt: number
|
|
158
|
-
staleAt: number
|
|
159
158
|
deleteAt?: number
|
|
160
159
|
}
|
|
161
160
|
|
|
@@ -168,7 +167,6 @@ export interface CacheGetResult {
|
|
|
168
167
|
cacheControlDirectives?: Record<string, unknown>
|
|
169
168
|
vary?: Record<string, string | string[]>
|
|
170
169
|
cachedAt: number
|
|
171
|
-
staleAt: number
|
|
172
170
|
deleteAt: number
|
|
173
171
|
}
|
|
174
172
|
|
package/lib/index.js
CHANGED
|
@@ -159,7 +159,7 @@ function wrapDispatch(dispatcher) {
|
|
|
159
159
|
upgrade: opts.upgrade ?? false,
|
|
160
160
|
follow: opts.follow ?? opts.redirect ?? 8,
|
|
161
161
|
error: opts.error ?? opts.throwOnError ?? true,
|
|
162
|
-
verify: opts.verify ?? false,
|
|
162
|
+
verify: opts.verify ?? { size: true, hash: false },
|
|
163
163
|
logger: opts.logger ?? null,
|
|
164
164
|
dns: opts.dns ?? true,
|
|
165
165
|
connect: opts.connect,
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -135,7 +135,6 @@ class CacheHandler extends DecoratorHandler {
|
|
|
135
135
|
etag: isEtagUsable(headers.etag) ? headers.etag : '',
|
|
136
136
|
vary,
|
|
137
137
|
cachedAt,
|
|
138
|
-
staleAt: 0,
|
|
139
138
|
// Handler state.
|
|
140
139
|
size: 0,
|
|
141
140
|
}
|
|
@@ -192,6 +191,12 @@ export default () => (dispatch) => (opts, handler) => {
|
|
|
192
191
|
const onlyIfCached =
|
|
193
192
|
typeof rawCacheControl === 'string' && rawCacheControl.includes('only-if-cached')
|
|
194
193
|
|
|
194
|
+
// RFC 9111 Section 5.4: Pragma: no-cache should be treated as
|
|
195
|
+
// Cache-Control: no-cache when Cache-Control is absent.
|
|
196
|
+
if (rawCacheControl == null && opts?.headers?.pragma === 'no-cache') {
|
|
197
|
+
cacheControlDirectives['no-cache'] = true
|
|
198
|
+
}
|
|
199
|
+
|
|
195
200
|
if (cacheControlDirectives['no-transform']) {
|
|
196
201
|
// Do nothing. We don't transform requests...
|
|
197
202
|
}
|
|
@@ -223,6 +228,37 @@ export default () => (dispatch) => (opts, handler) => {
|
|
|
223
228
|
}
|
|
224
229
|
}
|
|
225
230
|
|
|
231
|
+
// RFC 9111 Section 3.5: A shared cache must not use a cached response to a
|
|
232
|
+
// request with Authorization unless the response includes a public directive.
|
|
233
|
+
if (entry && opts.headers?.authorization && !entry.cacheControlDirectives?.public) {
|
|
234
|
+
entry = undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// RFC 9110 Section 13: Evaluate conditional request headers against cached entry.
|
|
238
|
+
if (entry && opts.headers?.['if-none-match']) {
|
|
239
|
+
if (entry.etag && weakMatch(opts.headers['if-none-match'], entry.etag)) {
|
|
240
|
+
return serveFromCache({ statusCode: 304, headers: entry.headers }, opts, handler)
|
|
241
|
+
}
|
|
242
|
+
// Etag didn't match — bypass to origin.
|
|
243
|
+
entry = undefined
|
|
244
|
+
} else if (entry && opts.headers?.['if-modified-since']) {
|
|
245
|
+
const lastModified = entry.headers?.['last-modified']
|
|
246
|
+
if (lastModified && new Date(lastModified) <= new Date(opts.headers['if-modified-since'])) {
|
|
247
|
+
return serveFromCache({ statusCode: 304, headers: entry.headers }, opts, handler)
|
|
248
|
+
}
|
|
249
|
+
// No last-modified or modified since — bypass to origin.
|
|
250
|
+
entry = undefined
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
opts.headers?.['if-match'] ||
|
|
255
|
+
opts.headers?.['if-unmodified-since'] ||
|
|
256
|
+
opts.headers?.['if-range']
|
|
257
|
+
) {
|
|
258
|
+
// TODO (fix): evaluate these conditional headers against cached entry.
|
|
259
|
+
return dispatch(opts, handler)
|
|
260
|
+
}
|
|
261
|
+
|
|
226
262
|
if (!entry && !onlyIfCached) {
|
|
227
263
|
return dispatch(
|
|
228
264
|
opts,
|
|
@@ -238,7 +274,11 @@ export default () => (dispatch) => (opts, handler) => {
|
|
|
238
274
|
)
|
|
239
275
|
}
|
|
240
276
|
|
|
241
|
-
|
|
277
|
+
return serveFromCache(entry ?? { statusCode: 504 }, opts, handler)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function serveFromCache(entry, opts, handler) {
|
|
281
|
+
const { statusCode, headers, trailers, body } = entry
|
|
242
282
|
|
|
243
283
|
let aborted = false
|
|
244
284
|
const abort = (reason) => {
|
|
@@ -275,6 +315,31 @@ export default () => (dispatch) => (opts, handler) => {
|
|
|
275
315
|
}
|
|
276
316
|
}
|
|
277
317
|
|
|
318
|
+
/**
|
|
319
|
+
* RFC 9110 Section 8.8.3.2: Weak comparison — two etags match if their
|
|
320
|
+
* opaque-tags match, ignoring the W/ prefix.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} ifNoneMatch - The If-None-Match header value (may contain multiple etags)
|
|
323
|
+
* @param {string} etag - The cached etag
|
|
324
|
+
* @returns {boolean}
|
|
325
|
+
*/
|
|
326
|
+
function weakMatch(ifNoneMatch, etag) {
|
|
327
|
+
if (ifNoneMatch === '*') {
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const normalize = (tag) => (tag.startsWith('W/') ? tag.slice(2) : tag)
|
|
332
|
+
const cached = normalize(etag)
|
|
333
|
+
|
|
334
|
+
for (const raw of ifNoneMatch.split(',')) {
|
|
335
|
+
if (normalize(raw.trim()) === cached) {
|
|
336
|
+
return true
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
|
|
278
343
|
/**
|
|
279
344
|
* Note: this deviates from the spec a little. Empty etags ("", W/"") are valid,
|
|
280
345
|
* however, including them in cached resposnes serves little to no purpose.
|
package/lib/interceptor/log.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DecoratorHandler } from '../utils.js'
|
|
2
2
|
|
|
3
|
-
const kGlobalIndex = Symbol('globalIndex')
|
|
4
|
-
const kGlobalArray = Symbol('globalArray')
|
|
3
|
+
const kGlobalIndex = Symbol.for('@nxtedition/nxt-undici#globalIndex')
|
|
4
|
+
const kGlobalArray = Symbol.for('@nxtedition/nxt-undici#globalArray')
|
|
5
5
|
|
|
6
6
|
class Handler extends DecoratorHandler {
|
|
7
7
|
#opts
|
|
@@ -35,7 +35,7 @@ class Handler extends DecoratorHandler {
|
|
|
35
35
|
this.#logger.debug('upstream request started')
|
|
36
36
|
this.#timing.created = this.#created + performance.timeOrigin
|
|
37
37
|
|
|
38
|
-
this[kGlobalArray] = globalThis
|
|
38
|
+
this[kGlobalArray] = globalThis[kGlobalArray] ??= []
|
|
39
39
|
this[kGlobalIndex] = this[kGlobalArray].push(this) - 1
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -177,11 +177,6 @@ class Handler extends DecoratorHandler {
|
|
|
177
177
|
onData(chunk) {
|
|
178
178
|
if (this.#pos != null) {
|
|
179
179
|
this.#pos += chunk.byteLength
|
|
180
|
-
|
|
181
|
-
if (this.#end != null && this.#pos > this.#end) {
|
|
182
|
-
this.#maybeError(new Error('Response body exceeded Content-Range'))
|
|
183
|
-
return false
|
|
184
|
-
}
|
|
185
180
|
}
|
|
186
181
|
|
|
187
182
|
if (this.#statusCode < 400) {
|
|
@@ -202,10 +197,6 @@ class Handler extends DecoratorHandler {
|
|
|
202
197
|
this.#trailers = trailers
|
|
203
198
|
|
|
204
199
|
if (this.#statusCode < 400) {
|
|
205
|
-
if (this.#end != null && this.#pos !== this.#end && this.#opts.method !== 'HEAD') {
|
|
206
|
-
this.#maybeError(new Error('Response body length mismatch with Content-Range'))
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
200
|
return super.onComplete(trailers)
|
|
210
201
|
}
|
|
211
202
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
|
-
import { DecoratorHandler } from '../utils.js'
|
|
2
|
+
import { DecoratorHandler, parseContentRange } from '../utils.js'
|
|
3
3
|
|
|
4
4
|
class Handler extends DecoratorHandler {
|
|
5
5
|
#verifyOpts
|
|
6
6
|
#contentMD5
|
|
7
|
-
#
|
|
7
|
+
#expectedSize
|
|
8
8
|
#hasher
|
|
9
9
|
#pos = 0
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ class Handler extends DecoratorHandler {
|
|
|
16
16
|
|
|
17
17
|
onConnect(abort) {
|
|
18
18
|
this.#contentMD5 = null
|
|
19
|
-
this.#
|
|
19
|
+
this.#expectedSize = null
|
|
20
20
|
this.#hasher = null
|
|
21
21
|
this.#pos = 0
|
|
22
22
|
|
|
@@ -25,7 +25,16 @@ class Handler extends DecoratorHandler {
|
|
|
25
25
|
|
|
26
26
|
onHeaders(statusCode, headers, resume) {
|
|
27
27
|
this.#contentMD5 = this.#verifyOpts.hash ? headers['content-md5'] : null
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
if (this.#verifyOpts.size) {
|
|
30
|
+
const contentRange = parseContentRange(headers['content-range'])
|
|
31
|
+
if (contentRange?.start != null && contentRange?.end != null) {
|
|
32
|
+
this.#expectedSize = contentRange.end - contentRange.start
|
|
33
|
+
} else if (headers['content-length'] != null) {
|
|
34
|
+
this.#expectedSize = Number(headers['content-length'])
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
this.#hasher = this.#contentMD5 != null ? crypto.createHash('md5') : null
|
|
30
39
|
|
|
31
40
|
return super.onHeaders(statusCode, headers, resume)
|
|
@@ -35,16 +44,26 @@ class Handler extends DecoratorHandler {
|
|
|
35
44
|
this.#pos += chunk.length
|
|
36
45
|
this.#hasher?.update(chunk)
|
|
37
46
|
|
|
47
|
+
if (this.#expectedSize != null && this.#pos > this.#expectedSize) {
|
|
48
|
+
super.onError(
|
|
49
|
+
Object.assign(new Error('Response body exceeded Content-Range'), {
|
|
50
|
+
expected: this.#expectedSize,
|
|
51
|
+
actual: this.#pos,
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
return super.onData(chunk)
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
onComplete(trailers) {
|
|
42
61
|
const contentMD5 = this.#hasher?.digest('base64')
|
|
43
62
|
|
|
44
|
-
if (this.#
|
|
63
|
+
if (this.#expectedSize != null && this.#pos !== this.#expectedSize) {
|
|
45
64
|
super.onError(
|
|
46
|
-
Object.assign(new Error('Response
|
|
47
|
-
expected:
|
|
65
|
+
Object.assign(new Error('Response body size mismatch'), {
|
|
66
|
+
expected: this.#expectedSize,
|
|
48
67
|
actual: this.#pos,
|
|
49
68
|
}),
|
|
50
69
|
)
|
|
@@ -2,7 +2,7 @@ import { DatabaseSync } from 'node:sqlite'
|
|
|
2
2
|
import { parseRangeHeader, getFastNow } from './utils.js'
|
|
3
3
|
|
|
4
4
|
// Bump version when the URL key format or schema changes to invalidate old caches.
|
|
5
|
-
const VERSION =
|
|
5
|
+
const VERSION = 10
|
|
6
6
|
|
|
7
7
|
/** @typedef {{ purgeStale: () => void } } */
|
|
8
8
|
const stores = new Set()
|
|
@@ -33,7 +33,6 @@ const stores = new Set()
|
|
|
33
33
|
* etag?: string
|
|
34
34
|
* cacheControlDirectives?: string
|
|
35
35
|
* cachedAt: number
|
|
36
|
-
* staleAt: number
|
|
37
36
|
* deleteAt: number
|
|
38
37
|
* }} SqliteStoreValue
|
|
39
38
|
*/
|
|
@@ -96,8 +95,7 @@ export class SqliteCacheStore {
|
|
|
96
95
|
cacheControlDirectives TEXT NULL,
|
|
97
96
|
etag TEXT NULL,
|
|
98
97
|
vary TEXT NULL,
|
|
99
|
-
cachedAt INTEGER NOT NULL
|
|
100
|
-
staleAt INTEGER NOT NULL
|
|
98
|
+
cachedAt INTEGER NOT NULL
|
|
101
99
|
);
|
|
102
100
|
|
|
103
101
|
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_getValuesQuery ON cacheInterceptorV${VERSION}(url, method, start, deleteAt);
|
|
@@ -117,8 +115,7 @@ export class SqliteCacheStore {
|
|
|
117
115
|
etag,
|
|
118
116
|
cacheControlDirectives,
|
|
119
117
|
vary,
|
|
120
|
-
cachedAt
|
|
121
|
-
staleAt
|
|
118
|
+
cachedAt
|
|
122
119
|
FROM cacheInterceptorV${VERSION}
|
|
123
120
|
WHERE
|
|
124
121
|
url = ?
|
|
@@ -143,9 +140,8 @@ export class SqliteCacheStore {
|
|
|
143
140
|
etag,
|
|
144
141
|
cacheControlDirectives,
|
|
145
142
|
vary,
|
|
146
|
-
cachedAt
|
|
147
|
-
|
|
148
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
143
|
+
cachedAt
|
|
144
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
149
145
|
`)
|
|
150
146
|
|
|
151
147
|
this.#deleteExpiredValuesQuery = this.#db.prepare(
|
|
@@ -255,7 +251,6 @@ export class SqliteCacheStore {
|
|
|
255
251
|
: null,
|
|
256
252
|
vary: value.vary ? JSON.stringify(value.vary) : null,
|
|
257
253
|
cachedAt: value.cachedAt,
|
|
258
|
-
staleAt: value.staleAt,
|
|
259
254
|
})
|
|
260
255
|
}
|
|
261
256
|
|
|
@@ -286,7 +281,6 @@ export class SqliteCacheStore {
|
|
|
286
281
|
cacheControlDirectives,
|
|
287
282
|
vary,
|
|
288
283
|
cachedAt,
|
|
289
|
-
staleAt,
|
|
290
284
|
} = this.#insertBatch[n++]
|
|
291
285
|
this.#insertValueQuery.run(
|
|
292
286
|
url,
|
|
@@ -302,7 +296,6 @@ export class SqliteCacheStore {
|
|
|
302
296
|
cacheControlDirectives,
|
|
303
297
|
vary,
|
|
304
298
|
cachedAt,
|
|
305
|
-
staleAt,
|
|
306
299
|
)
|
|
307
300
|
if ((n & 0xf) === 0 && performance.now() - startTime > 10) {
|
|
308
301
|
break
|
|
@@ -463,7 +456,6 @@ function makeResult(value) {
|
|
|
463
456
|
? JSON.parse(value.cacheControlDirectives)
|
|
464
457
|
: undefined,
|
|
465
458
|
cachedAt: value.cachedAt,
|
|
466
|
-
staleAt: value.staleAt,
|
|
467
459
|
deleteAt: value.deleteAt,
|
|
468
460
|
}
|
|
469
461
|
}
|
|
@@ -503,7 +495,7 @@ function assertCacheValue(value) {
|
|
|
503
495
|
throw new TypeError(`expected value to be object, got ${printType(value)}`)
|
|
504
496
|
}
|
|
505
497
|
|
|
506
|
-
for (const property of ['statusCode', 'cachedAt', '
|
|
498
|
+
for (const property of ['statusCode', 'cachedAt', 'deleteAt']) {
|
|
507
499
|
if (typeof value[property] !== 'number') {
|
|
508
500
|
throw new TypeError(
|
|
509
501
|
`expected value.${property} to be number, got ${printType(value[property])} [${value[property]}]`,
|
package/lib/utils.js
CHANGED
|
@@ -163,14 +163,6 @@ export function parseOrigin(url) {
|
|
|
163
163
|
return url
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
export class AbortError extends Error {
|
|
167
|
-
constructor(message) {
|
|
168
|
-
super(message ?? 'The operation was aborted')
|
|
169
|
-
this.code = 'ABORT_ERR'
|
|
170
|
-
this.name = 'AbortError'
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
166
|
export function isStream(obj) {
|
|
175
167
|
return (
|
|
176
168
|
obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
|