Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ export interface ProxyOptions {
export interface CacheOptions {
store?: CacheStore
maxEntrySize?: number
// Opt in to reading entries cached from Cache-Control: private responses
// (private redirects). Without it such entries are bypassed on read.
private?: boolean
}

export interface VerifyOptions {
Expand Down Expand Up @@ -156,6 +159,7 @@ export interface CacheValue {
vary?: Record<string, string | string[]>
cachedAt: number
deleteAt?: number
private?: boolean
}

export interface CacheGetResult {
Expand All @@ -168,6 +172,7 @@ export interface CacheGetResult {
vary?: Record<string, string | string[]>
cachedAt: number
deleteAt: number
private?: boolean
}

export interface CacheStore {
Expand Down
23 changes: 22 additions & 1 deletion lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,23 @@ class CacheHandler extends DecoratorHandler {
return super.onHeaders(statusCode, headers, resume)
}

if (cacheControlDirectives.private || cacheControlDirectives['no-store']) {
if (cacheControlDirectives['no-store']) {
return super.onHeaders(statusCode, headers, resume)
}

// Cache redirections even if private. Of these, only 307 can reach this
// point today (the admit guard at the top of onHeaders allows 307/200/206)
// — the full redirect set is listed so broadening the admit list later
// doesn't silently drop private redirects.
if (
cacheControlDirectives['private'] &&
statusCode !== 307 &&
statusCode !== 308 &&
statusCode !== 300 &&
statusCode !== 301 &&
statusCode !== 302 &&
statusCode !== 303
) {
return super.onHeaders(statusCode, headers, resume)
}

Expand Down Expand Up @@ -173,6 +189,7 @@ class CacheHandler extends DecoratorHandler {
etag: isEtagUsable(headers.etag) ? headers.etag : '',
vary,
cachedAt,
private: Boolean(cacheControlDirectives['private']),
// Handler state.
size: 0,
}
Expand Down Expand Up @@ -289,6 +306,10 @@ export default () => (dispatch) => (opts, handler) => {
}
}

if (entry?.private && !opts.cache.private) {
entry = null
}
Comment thread
ronag marked this conversation as resolved.
Comment on lines +309 to +311

// RFC 9111 Section 3.5: A shared cache must not use a cached response to a
// request with Authorization unless the response includes a public directive.
if (entry && opts.headers?.authorization && !entry.cacheControlDirectives?.public) {
Expand Down
22 changes: 16 additions & 6 deletions lib/sqlite-cache-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DatabaseSync } from 'node:sqlite'
import { parseRangeHeader, getFastNow } from './utils.js'

// Bump version when the URL key format or schema changes to invalidate old caches.
const VERSION = 10
const VERSION = 11

/** @typedef {{ gc: () => void, clear: () => void } } */
const stores = new Set()
Expand Down Expand Up @@ -44,6 +44,7 @@ const stores = new Set()
* cacheControlDirectives?: string
* cachedAt: number
* deleteAt: number
* private: 0 | 1
* }} SqliteStoreValue
Comment thread
ronag marked this conversation as resolved.
*/
export class SqliteCacheStore {
Expand Down Expand Up @@ -115,7 +116,8 @@ export class SqliteCacheStore {
cacheControlDirectives TEXT NULL,
etag TEXT NULL,
vary TEXT NULL,
cachedAt INTEGER NOT NULL
cachedAt INTEGER NOT NULL,
private BOOLEAN NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_getValuesQuery ON cacheInterceptorV${VERSION}(url, method, start, deleteAt);
Expand All @@ -135,7 +137,8 @@ export class SqliteCacheStore {
etag,
cacheControlDirectives,
vary,
cachedAt
cachedAt,
private
FROM cacheInterceptorV${VERSION}
WHERE
url = ?
Expand All @@ -160,8 +163,9 @@ export class SqliteCacheStore {
etag,
cacheControlDirectives,
vary,
cachedAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
cachedAt,
private
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)

this.#deleteExpiredValuesQuery = this.#db.prepare(
Expand Down Expand Up @@ -252,7 +256,7 @@ export class SqliteCacheStore {

/**
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>, start: number, end: number }} value
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>, start: number, end: number, private?: boolean }} value
*/
set(key, value) {
assertCacheKey(key)
Expand Down Expand Up @@ -313,6 +317,8 @@ export class SqliteCacheStore {
: null,
vary: value.vary ? JSON.stringify(value.vary) : null,
cachedAt: value.cachedAt,
// node:sqlite cannot bind a JS boolean; store as 0/1.
private: value.private ? 1 : 0,
})
}

Expand Down Expand Up @@ -343,6 +349,8 @@ export class SqliteCacheStore {
cacheControlDirectives,
vary,
cachedAt,
// 'private' is a reserved word — bind under a safe name.
private: isPrivate,
} = this.#insertBatch[n++]
this.#insertValueQuery.run(
url,
Expand All @@ -358,6 +366,7 @@ export class SqliteCacheStore {
cacheControlDirectives,
vary,
cachedAt,
isPrivate,
)
if (!final && (n & 0xf) === 0 && performance.now() - startTime > 10) {
break
Expand Down Expand Up @@ -580,6 +589,7 @@ function makeResult(value) {
? JSON.parse(value.cacheControlDirectives)
: undefined,
cachedAt: value.cachedAt,
private: Boolean(value.private),
deleteAt: value.deleteAt,
}
}
Expand Down