Skip to content
Merged
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
19 changes: 16 additions & 3 deletions docs/docs/api/CacheStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ Parameters:
Returns: `GetResult | Promise<GetResult | undefined> | undefined` - If the request is cached, the cached response is returned. If the request's method is anything other than HEAD, the response is also returned.
If the request isn't cached, `undefined` is returned.

The `get` method may return a `Promise` for async cache stores (e.g. Redis-backed or remote stores). The cache interceptor handles both synchronous and asynchronous return values, including in revalidation paths (304 Not Modified handling and stale-while-revalidate background revalidation).

Response properties:

* **response** `CacheValue` - The cached response data.
* **body** `Readable | undefined` - The response's body.
* **body** `Readable | Iterable<Buffer> | undefined` - The response's body. This can be an array of `Buffer` chunks (with a `.values()` method) or a `Readable` stream. Both formats are supported in all code paths, including 304 revalidation.

### Function: `createWriteStream`

Expand Down Expand Up @@ -98,8 +100,11 @@ This is an interface containing the majority of a response's data (minus the bod

### Property `vary`

`Record<string, string | string[]> | undefined` - The headers defined by the response's `Vary` header
and their respective values for later comparison
`Record<string, string | string[] | null> | undefined` - The headers defined by the response's `Vary` header
and their respective values for later comparison. Values are `null` when the
header specified in `Vary` was not present in the original request. These `null`
values are automatically filtered out during revalidation so they are not sent
as request headers.

For example, for a response like
```
Expand All @@ -116,6 +121,14 @@ This would be
}
```

If the original request did not include the `accepts` header:
```js
{
'content-encoding': 'utf8',
accepts: null
}
```

### Property `cachedAt`

`number` - Time in millis that this value was cached.
Expand Down
119 changes: 77 additions & 42 deletions lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,57 +193,92 @@ class CacheHandler {
// Not modified, re-use the cached value
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
if (statusCode === 304) {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const cachedValue = this.#store.get(this.#cacheKey)
if (!cachedValue) {
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
return downstreamOnHeaders()
}

// Re-use the cached value: statuscode, statusmessage, headers and body
value.statusCode = cachedValue.statusCode
value.statusMessage = cachedValue.statusMessage
value.etag = cachedValue.etag
value.headers = { ...cachedValue.headers, ...strippedHeaders }
const handle304 = (cachedValue) => {
if (!cachedValue) {
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
return downstreamOnHeaders()
}

downstreamOnHeaders()
// Re-use the cached value: statuscode, statusmessage, headers and body
value.statusCode = cachedValue.statusCode
value.statusMessage = cachedValue.statusMessage
value.etag = cachedValue.etag
value.headers = { ...cachedValue.headers, ...strippedHeaders }

this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
downstreamOnHeaders()

if (!this.#writeStream || !cachedValue?.body) {
return
}
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)

const bodyIterator = cachedValue.body.values()
if (!this.#writeStream || !cachedValue?.body) {
return
}

const streamCachedBody = () => {
for (const chunk of bodyIterator) {
const full = this.#writeStream.write(chunk) === false
this.#handler.onResponseData?.(controller, chunk)
// when stream is full stop writing until we get a 'drain' event
if (full) {
break
if (typeof cachedValue.body.values === 'function') {
const bodyIterator = cachedValue.body.values()

const streamCachedBody = () => {
for (const chunk of bodyIterator) {
const full = this.#writeStream.write(chunk) === false
this.#handler.onResponseData?.(controller, chunk)
// when stream is full stop writing until we get a 'drain' event
if (full) {
break
}
}
}
}
}

this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('drain', () => {
this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('drain', () => {
streamCachedBody()
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})

streamCachedBody()
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})
} else if (typeof cachedValue.body.on === 'function') {
// Readable stream body (e.g. from async/remote cache stores)
cachedValue.body
.on('data', (chunk) => {
this.#writeStream.write(chunk)
this.#handler.onResponseData?.(controller, chunk)
})
.on('end', () => {
this.#writeStream.end()
})
.on('error', () => {
this.#writeStream = undefined
this.#store.delete(this.#cacheKey)
})

this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})
}
}

streamCachedBody()
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const result = this.#store.get(this.#cacheKey)
if (result && typeof result.then === 'function') {
result.then(handle304)
} else {
handle304(result)
}
} else {
if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
value.etag = resHeaders.etag
Expand Down
18 changes: 10 additions & 8 deletions lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ function handleResult (

// Start background revalidation (fire-and-forget)
queueMicrotask(() => {
let headers = {
const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
}
Expand All @@ -302,9 +302,10 @@ function handleResult (
}

if (result.vary) {
headers = {
...headers,
...result.vary
for (const key in result.vary) {
if (result.vary[key] != null) {
headers[key] = result.vary[key]
}
}
}

Expand Down Expand Up @@ -335,7 +336,7 @@ function handleResult (
withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
}

let headers = {
const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
}
Expand All @@ -345,9 +346,10 @@ function handleResult (
}

if (result.vary) {
headers = {
...headers,
...result.vary
for (const key in result.vary) {
if (result.vary[key] != null) {
headers[key] = result.vary[key]
}
}
}

Expand Down
Loading
Loading