Skip to content

Commit 6ce8636

Browse files
committed
fix: capture cache tags during request handling and don't rely on tag manifest created for prerendered pages
1 parent bd713be commit 6ce8636

File tree

8 files changed

+49
-79
lines changed

8 files changed

+49
-79
lines changed

CONTRIBUTING.md

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ For a simple next.js app
3838
```
3939
/___netlify-server-handler
4040
├── .netlify
41-
│ ├── tags-manifest.json
4241
│ └── dist // the compiled runtime code
4342
│ └── run
4443
│ ├── handlers

src/build/content/server.ts

-41
Original file line numberDiff line numberDiff line change
@@ -311,47 +311,6 @@ export const copyNextDependencies = async (ctx: PluginContext): Promise<void> =>
311311
})
312312
}
313313

314-
export const writeTagsManifest = async (ctx: PluginContext): Promise<void> => {
315-
const manifest = await ctx.getPrerenderManifest()
316-
317-
const routes = Object.entries(manifest.routes).map(async ([route, definition]) => {
318-
let tags
319-
320-
// app router
321-
if (definition.dataRoute?.endsWith('.rsc')) {
322-
const path = join(ctx.publishDir, `server/app/${route === '/' ? '/index' : route}.meta`)
323-
try {
324-
const file = await readFile(path, 'utf-8')
325-
const meta = JSON.parse(file)
326-
tags = meta.headers['x-next-cache-tags']
327-
} catch {
328-
// Parallel route default layout has no prerendered page, so don't warn about it
329-
if (!definition.dataRoute?.endsWith('/default.rsc')) {
330-
console.log(`Unable to read cache tags for: ${path}`)
331-
}
332-
}
333-
}
334-
335-
// pages router
336-
if (definition.dataRoute?.endsWith('.json')) {
337-
tags = `_N_T_${route}`
338-
}
339-
340-
// route handler
341-
if (definition.dataRoute === null) {
342-
tags = definition.initialHeaders?.['x-next-cache-tags']
343-
}
344-
345-
return [route, tags]
346-
})
347-
348-
await writeFile(
349-
join(ctx.serverHandlerDir, '.netlify/tags-manifest.json'),
350-
JSON.stringify(Object.fromEntries(await Promise.all(routes))),
351-
'utf-8',
352-
)
353-
}
354-
355314
/**
356315
* Generates a copy of the middleware manifest without any middleware in it. We
357316
* do this because we'll run middleware in an edge function, and we don't want

src/build/functions/server.ts

-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
copyNextDependencies,
1111
copyNextServerCode,
1212
verifyHandlerDirStructure,
13-
writeTagsManifest,
1413
} from '../content/server.js'
1514
import { PluginContext, SERVER_HANDLER_NAME } from '../plugin-context.js'
1615

@@ -138,7 +137,6 @@ export const createServerHandler = async (ctx: PluginContext) => {
138137

139138
await copyNextServerCode(ctx)
140139
await copyNextDependencies(ctx)
141-
await writeTagsManifest(ctx)
142140
await copyHandlerDependencies(ctx)
143141
await writeHandlerManifest(ctx)
144142
await writePackageMetadata(ctx)

src/run/config.ts

-6
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,3 @@ export const setRunConfig = (config: NextConfigComplete) => {
3838
// set config
3939
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config)
4040
}
41-
42-
export type TagsManifest = Record<string, string>
43-
44-
export const getTagsManifest = async (): Promise<TagsManifest> => {
45-
return JSON.parse(await readFile(resolve(PLUGIN_DIR, '.netlify/tags-manifest.json'), 'utf-8'))
46-
}

src/run/handlers/cache.cts

+43
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,44 @@ export class NetlifyCacheHandler implements CacheHandler {
109109
return restOfRouteValue
110110
}
111111

112+
private captureCacheTags(cacheValue: NetlifyIncrementalCacheValue | null, key: string) {
113+
if (!cacheValue) {
114+
return
115+
}
116+
117+
const requestContext = getRequestContext()
118+
// Bail if we can't get request context
119+
if (!requestContext) {
120+
return
121+
}
122+
123+
// Bail if we already have cache tags - `captureCacheTags()` is called on both `CacheHandler.get` and `CacheHandler.set`
124+
// that's because `CacheHandler.get` might not have a cache value (cache miss or on-demand revalidation) in which case
125+
// response is generated in blocking way and we need to capture cache tags from the cache value we are setting.
126+
// If both `CacheHandler.get` and `CacheHandler.set` are called in the same request, we want to use cache tags from
127+
// first `CacheHandler.get` and not from following `CacheHandler.set` as this is pattern for Stale-while-revalidate behavior
128+
// and stale response is served while new one is generated.
129+
if (requestContext.responseCacheTags) {
130+
return
131+
}
132+
133+
if (
134+
cacheValue.kind === 'PAGE' ||
135+
cacheValue.kind === 'APP_PAGE' ||
136+
cacheValue.kind === 'ROUTE'
137+
) {
138+
if (cacheValue.headers?.[NEXT_CACHE_TAGS_HEADER]) {
139+
const cacheTags = (cacheValue.headers[NEXT_CACHE_TAGS_HEADER] as string).split(',')
140+
requestContext.responseCacheTags = cacheTags
141+
} else if (cacheValue.kind === 'PAGE' && typeof cacheValue.pageData === 'object') {
142+
// pages router doesn't have cache tags headers in PAGE cache value
143+
// so we need to generate appropriate cache tags for it
144+
const cacheTags = [`_N_T_${key === '/index' ? '/' : key}`]
145+
requestContext.responseCacheTags = cacheTags
146+
}
147+
}
148+
}
149+
112150
private async injectEntryToPrerenderManifest(
113151
key: string,
114152
revalidate: NetlifyCachedPageValue['revalidate'],
@@ -176,6 +214,7 @@ export class NetlifyCacheHandler implements CacheHandler {
176214
}
177215

178216
this.captureResponseCacheLastModified(blob, key, span)
217+
this.captureCacheTags(blob.value, key)
179218

180219
switch (blob.value?.kind) {
181220
case 'FETCH':
@@ -273,6 +312,10 @@ export class NetlifyCacheHandler implements CacheHandler {
273312

274313
const value = this.transformToStorableObject(data, context)
275314

315+
// if previous CacheHandler.get call returned null (page was either never rendered on was on-demand revalidated)
316+
// and we didn't yet capture cache tags, we try to get cache tags from freshly produced cache value
317+
this.captureCacheTags(value, key)
318+
276319
await this.blobStore.setJSON(blobKey, {
277320
lastModified,
278321
value,

src/run/handlers/request-context.cts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type RequestContext = {
1010
captureServerTiming: boolean
1111
responseCacheGetLastModified?: number
1212
responseCacheKey?: string
13+
responseCacheTags?: string[]
1314
usedFsRead?: boolean
1415
didPagesRouterOnDemandRevalidate?: boolean
1516
serverTiming?: string

src/run/handlers/server.ts

+2-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Context } from '@netlify/functions'
55
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
66
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
77

8-
import { getTagsManifest, TagsManifest } from '../config.js'
98
import {
109
adjustDateHeader,
1110
setCacheControlHeaders,
@@ -20,7 +19,7 @@ import { getTracer } from './tracer.cjs'
2019

2120
const nextImportPromise = import('../next.cjs')
2221

23-
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete, tagsManifest: TagsManifest
22+
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete
2423

2524
/**
2625
* When Next.js proxies requests externally, it writes the response back as-is.
@@ -60,16 +59,6 @@ export default async (request: Request, context: FutureContext) => {
6059
const { getRunConfig, setRunConfig } = await import('../config.js')
6160
nextConfig = await getRunConfig()
6261
setRunConfig(nextConfig)
63-
tagsManifest = await getTagsManifest()
64-
span.setAttributes(
65-
Object.entries(tagsManifest).reduce(
66-
(acc, [key, value]) => {
67-
acc[`tagsManifest.${key}`] = value
68-
return acc
69-
},
70-
{} as Record<string, string>,
71-
),
72-
)
7362

7463
const { getMockedRequestHandlers } = await nextImportPromise
7564
const url = new URL(request.url)
@@ -124,7 +113,7 @@ export default async (request: Request, context: FutureContext) => {
124113
await adjustDateHeader({ headers: response.headers, request, span, tracer, requestContext })
125114

126115
setCacheControlHeaders(response.headers, request, requestContext)
127-
setCacheTagsHeaders(response.headers, request, tagsManifest, requestContext)
116+
setCacheTagsHeaders(response.headers, requestContext)
128117
setVaryHeaders(response.headers, request, nextConfig)
129118
setCacheStatusHeader(response.headers)
130119

src/run/headers.ts

+3-16
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
33

44
import { encodeBlobKey } from '../shared/blobkey.js'
55

6-
import type { TagsManifest } from './config.js'
76
import type { RequestContext } from './handlers/request-context.cjs'
87
import type { RuntimeTracer } from './handlers/tracer.cjs'
98
import { getRegionalBlobStore } from './regional-blob-store.cjs'
@@ -275,21 +274,9 @@ export const setCacheControlHeaders = (
275274
}
276275
}
277276

278-
function getCanonicalPathFromCacheKey(cacheKey: string | undefined): string | undefined {
279-
return cacheKey === '/index' ? '/' : cacheKey
280-
}
281-
282-
export const setCacheTagsHeaders = (
283-
headers: Headers,
284-
request: Request,
285-
manifest: TagsManifest,
286-
requestContext: RequestContext,
287-
) => {
288-
const path =
289-
getCanonicalPathFromCacheKey(requestContext.responseCacheKey) ?? new URL(request.url).pathname
290-
const tags = manifest[path]
291-
if (tags !== undefined) {
292-
headers.set('netlify-cache-tag', tags)
277+
export const setCacheTagsHeaders = (headers: Headers, requestContext: RequestContext) => {
278+
if (requestContext.responseCacheTags) {
279+
headers.set('netlify-cache-tag', requestContext.responseCacheTags.join(','))
293280
}
294281
}
295282

0 commit comments

Comments
 (0)