@@ -4,7 +4,6 @@ import type { StoredComposableCacheEntry } from "@/types/cache";
44import type { InternalEvent , InternalResult } from "@/types/open-next" ;
55import type {
66 CacheEntryType ,
7- CachedFile ,
87 CachedFetchValue ,
98 CacheValue ,
109 OpenNextHandlerOptions ,
@@ -13,6 +12,7 @@ import type {
1312
1413import { createGenericHandler } from "../core/createGenericHandler.js" ;
1514import { resolveCdnInvalidation , resolveIncrementalCache , resolveTagCache } from "../core/resolve.js" ;
15+ import { computeEntryCacheControl } from "../utils/cache-control.js" ;
1616import { getTagsFromValue , isStale , writeTags } from "../utils/cache.js" ;
1717import { runWithOpenNextRequestContext } from "../utils/promise.js" ;
1818import { toReadableStream } from "../utils/stream.js" ;
@@ -130,9 +130,9 @@ async function handleGet(
130130 } ;
131131 }
132132
133- // `getTagsFromValue` also strips the internal `x-next-cache-tags` header from the entry , so
134- // the tags are derived for every hit - including the ones bypassing the tag cache - or that
135- // header would be handed back to Next.js and echoed to the client.
133+ // The tags are also used to make the response purgeable , so they are derived for every hit,
134+ // including the ones bypassing the tag cache. `getTagsFromValue` additionally strips the
135+ // internal `x-next-cache-tags` header, which would otherwise be echoed to the client.
136136 let tags : string [ ] = [ ...additionalTags ] ;
137137
138138 if ( cacheType === "cache" ) {
@@ -145,6 +145,8 @@ async function handleGet(
145145 tags = [ ...tags , ...( composableValue . tags ?? [ ] ) ] ;
146146 }
147147
148+ let isEntryStale = false ;
149+
148150 if ( ! result . shouldBypassTagCache ) {
149151 const lastModified = result . lastModified ?? Date . now ( ) ;
150152
@@ -166,13 +168,15 @@ async function handleGet(
166168 }
167169
168170 // Check if the cache entry is stale (valid but needs background revalidation)
169- const _isStale = tags . length > 0 ? await isStale ( key , tags , lastModified ) : false ;
170- if ( _isStale ) {
171+ isEntryStale = tags . length > 0 ? await isStale ( key , tags , lastModified ) : false ;
172+ if ( isEntryStale ) {
171173 result . lastModified = 1 ;
172174 }
173175 }
174176
175- return buildCacheGetResponse ( result ) ;
177+ // We default to the entry key when no tag is found, so that page router based entries can also
178+ // be purged this way.
179+ return buildCacheGetResponse ( result , isEntryStale , tags . length > 0 ? tags : [ key ] ) ;
176180 } catch ( e ) {
177181 error ( "Failed to get cache entry" , e ) ;
178182 return buildErrorResponse ( "Failed to get cache entry" , 500 ) ;
@@ -409,14 +413,24 @@ async function handleRevalidateTags(body?: Buffer): Promise<InternalResult> {
409413// Cache GET response builder //
410414/////////////////////////////
411415
412- function buildCacheGetResponse ( result : WithLastModified < CacheValue < CacheEntryType > > ) : InternalResult {
416+ function buildCacheGetResponse (
417+ result : WithLastModified < CacheValue < CacheEntryType > > ,
418+ isStaleFromTagCache : boolean ,
419+ tags : string [ ]
420+ ) : InternalResult {
413421 const value = result . value ! ;
414422
415423 const headers : Record < string , string | string [ ] > = {
416424 "x-opennext-cache-found" : "true" ,
417- "Cache-Control" : "no-store" ,
425+ "Cache-Control" : computeEntryCacheControl ( value , result . lastModified , isStaleFromTagCache ) ,
418426 } ;
419427
428+ // The `Cache-Control` above lets an HTTP cache store this response, it can only be invalidated
429+ // through a purge keyed on these tags. See `computeEntryCacheControl`.
430+ if ( tags . length > 0 ) {
431+ headers [ "cache-tag" ] = tags . join ( "," ) ;
432+ }
433+
420434 if ( result . lastModified !== undefined ) {
421435 headers [ "x-opennext-cache-last-modified" ] = String ( result . lastModified ) ;
422436 }
@@ -425,11 +439,11 @@ function buildCacheGetResponse(result: WithLastModified<CacheValue<CacheEntryTyp
425439 }
426440
427441 if ( "kind" in value && value . kind === "FETCH" ) {
428- return buildFetchResponse ( value as CachedFetchValue , headers ) ;
442+ return buildFetchResponse ( value as CacheValue < "fetch" > , headers ) ;
429443 }
430444
431445 if ( "type" in value ) {
432- return buildCachedFileResponse ( value as CachedFile , headers ) ;
446+ return buildCachedFileResponse ( value as CacheValue < "cache" > , headers ) ;
433447 }
434448
435449 return buildComposableResponse ( value as StoredComposableCacheEntry , headers ) ;
0 commit comments