Skip to content

Commit

Permalink
test: add cache testing suite
Browse files Browse the repository at this point in the history
Closes nodejs#3852
Closes nodejs#3869

Signed-off-by: flakey5 <[email protected]>
  • Loading branch information
flakey5 committed Nov 25, 2024
1 parent 71b6b0b commit 1042386
Show file tree
Hide file tree
Showing 152 changed files with 69,243 additions and 23 deletions.
2 changes: 1 addition & 1 deletion docs/docs/api/CacheStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The `MemoryCacheStore` stores the responses in-memory.

- `maxCount` - The maximum amount of responses to store. Default `Infinity`.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached.

todo DOCS
### `SqliteCacheStore`

The `SqliteCacheStore` stores the responses in a SQLite database.
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = [
ignores: [
'lib/llhttp',
'test/fixtures/wpt',
'test/fixtures/cache-tests',
'undici-fetch.js'
],
noJsx: true,
Expand Down
10 changes: 9 additions & 1 deletion lib/cache/sqlite-cache-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')

const VERSION = 2

// 2gb
const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000

/**
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
* @implements {CacheStore}
Expand All @@ -18,7 +21,7 @@ const VERSION = 2
* } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
*/
class SqliteCacheStore {
#maxEntrySize = Infinity
#maxEntrySize = MAX_ENTRY_SIZE
#maxCount = Infinity

/**
Expand Down Expand Up @@ -78,6 +81,11 @@ class SqliteCacheStore {
) {
throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
}

if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
}

this.#maxEntrySize = opts.maxEntrySize
}

Expand Down
67 changes: 52 additions & 15 deletions lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class CacheHandler {
*/
#cacheKey

/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']}
*/
#cacheType

/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
*/
Expand All @@ -39,9 +44,10 @@ class CacheHandler {
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
*/
constructor (opts, cacheKey, handler) {
const { store } = opts
const { store, type } = opts

this.#store = store
this.#cacheType = type
this.#cacheKey = cacheKey
this.#handler = handler
}
Expand Down Expand Up @@ -96,11 +102,17 @@ class CacheHandler {
}

const now = Date.now()
const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
const staleAt = determineStaleAt(this.#cacheType, now, headers, cacheControlDirectives)
if (staleAt) {
const varyDirectives = this.#cacheKey.headers && headers.vary
? parseVaryHeader(headers.vary, this.#cacheKey.headers)
: undefined
let varyDirectives
if (this.#cacheKey.headers && headers.vary) {
varyDirectives = parseVaryHeader(headers.vary, this.#cacheKey.headers)
if (!varyDirectives) {
// Parse error
return downstreamOnHeaders()
}
}

const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)

const strippedHeaders = stripNecessaryHeaders(headers, cacheControlDirectives)
Expand Down Expand Up @@ -178,6 +190,20 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
return false
}

if (typeof headers.age === 'string') {
const age = parseInt(headers.age)
if (isNaN(age) || age >= 2147483647) {
return false
}

if (
cacheControlDirectives?.['max-age'] &&
age >= cacheControlDirectives?.['max-age']
) {
return false
}
}

if (
cacheControlDirectives.private === true ||
cacheControlDirectives['no-cache'] === true ||
Expand All @@ -187,7 +213,7 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
}

// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
if (headers.vary === '*') {
if (headers.vary?.includes('*')) {
return false
}

Expand Down Expand Up @@ -216,19 +242,22 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
}

/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']}
* @param {number} now
* @param {Record<string, string | string[]>} headers
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
*
* @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
*/
function determineStaleAt (now, headers, cacheControlDirectives) {
// Prioritize s-maxage since we're a shared cache
// s-maxage > max-age > Expire
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
const sMaxAge = cacheControlDirectives['s-maxage']
if (sMaxAge) {
return now + (sMaxAge * 1000)
function determineStaleAt (cacheType, now, headers, cacheControlDirectives) {
if (cacheType === 'public') {
// Prioritize s-maxage since we're a shared cache
// s-maxage > max-age > Expire
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
const sMaxAge = cacheControlDirectives['s-maxage']
if (sMaxAge) {
return now + (sMaxAge * 1000)
}
}

if (cacheControlDirectives.immutable) {
Expand All @@ -241,9 +270,9 @@ function determineStaleAt (now, headers, cacheControlDirectives) {
return now + (maxAge * 1000)
}

if (headers.expire && typeof headers.expire === 'string') {
if (headers.expires && typeof headers.expires === 'string') {
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
const expiresDate = new Date(headers.expire)
const expiresDate = new Date(headers.expires)
if (expiresDate instanceof Date && Number.isFinite(expiresDate.valueOf())) {
return now + (Date.now() - expiresDate.getTime())
}
Expand Down Expand Up @@ -281,6 +310,14 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
function stripNecessaryHeaders (headers, cacheControlDirectives) {
const headersToRemove = ['connection']

if (headers['connection']) {
if (Array.isArray(headers['connection'])) {
headersToRemove.push(...headers['connection'])
} else {
headersToRemove.push(headers['connection'])
}
}

if (Array.isArray(cacheControlDirectives['no-cache'])) {
headersToRemove.push(...cacheControlDirectives['no-cache'])
}
Expand Down
20 changes: 14 additions & 6 deletions lib/util/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,8 @@ function parseCacheControlHeader (header) {
let key
let value
if (keyValueDelimiter !== -1) {
key = directive.substring(0, keyValueDelimiter).trim()
value = directive
.substring(keyValueDelimiter + 1)
.trim()
key = directive.substring(0, keyValueDelimiter).trimStart()
value = directive.substring(keyValueDelimiter + 1)
} else {
key = directive.trim()
}
Expand All @@ -129,10 +127,18 @@ function parseCacheControlHeader (header) {
case 's-maxage':
case 'stale-while-revalidate':
case 'stale-if-error': {
if (value === undefined) {
if (value === undefined || value[0] === ' ') {
continue
}

if (
value.length >= 2 &&
value[0] === '"' &&
value[value.length - 1] === '"'
) {
value = value.substring(1, value.length - 1)
}

const parsedValue = parseInt(value, 10)
// eslint-disable-next-line no-self-compare
if (parsedValue !== parsedValue) {
Expand Down Expand Up @@ -229,7 +235,7 @@ function parseCacheControlHeader (header) {
* @returns {Record<string, string | string[]>}
*/
function parseVaryHeader (varyHeader, headers) {
if (typeof varyHeader === 'string' && varyHeader === '*') {
if (typeof varyHeader === 'string' && varyHeader.includes('*')) {
return headers
}

Expand All @@ -243,6 +249,8 @@ function parseVaryHeader (varyHeader, headers) {

if (headers[trimmedHeader]) {
output[trimmedHeader] = headers[trimmedHeader]
} else {
return undefined
}
}

Expand Down
6 changes: 6 additions & 0 deletions test/cache-interceptor/cache-tests-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'

import { parentPort } from 'node:worker_threads'

await import('../fixtures/cache-tests/test-engine/server/server.mjs')
parentPort.postMessage('listening')
Loading

0 comments on commit 1042386

Please sign in to comment.