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 24, 2024
1 parent 0ea7897 commit abb9554
Show file tree
Hide file tree
Showing 152 changed files with 69,219 additions and 21 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 = 1

// 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 = 1
* } & 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
41 changes: 28 additions & 13 deletions lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ class CacheHandler extends DecoratorHandler {
*/
#cacheKey

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

/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
*/
Expand All @@ -40,11 +45,12 @@ class CacheHandler extends DecoratorHandler {
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
*/
constructor (opts, cacheKey, handler) {
const { store } = opts
const { store, type } = opts

super(handler)

this.#store = store
this.#cacheType = type
this.#cacheKey = cacheKey
this.#handler = handler
}
Expand Down Expand Up @@ -123,11 +129,17 @@ class CacheHandler extends DecoratorHandler {
}

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(
Expand Down Expand Up @@ -252,7 +264,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 @@ -281,19 +293,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('../util/cache.js').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 Down
20 changes: 14 additions & 6 deletions lib/util/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,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 @@ -131,10 +129,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 @@ -231,7 +237,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 @@ -245,6 +251,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 abb9554

Please sign in to comment.