Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ea6af55

Browse filesBrowse files
committed
fix: only set permament caching header when reading html file when its not during server initialization AND when read html is Next produced fully static html
1 parent 9fe6763 commit ea6af55
Copy full SHA for ea6af55

File tree

Expand file treeCollapse file tree

4 files changed

+67
-10
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+67
-10
lines changed

‎src/build/content/static.ts

Copy file name to clipboardExpand all lines: src/build/content/static.ts
+4-2Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,23 @@ export const copyStaticContent = async (ctx: PluginContext): Promise<void> => {
2727
})
2828

2929
const fallbacks = ctx.getFallbacks(await ctx.getPrerenderManifest())
30+
const fullyStaticPages = await ctx.getFullyStaticHtmlPages()
3031

3132
try {
3233
await mkdir(destDir, { recursive: true })
3334
await Promise.all(
3435
paths
35-
.filter((path) => !paths.includes(`${path.slice(0, -5)}.json`))
36+
.filter((path) => !path.endsWith('.json') && !paths.includes(`${path.slice(0, -5)}.json`))
3637
.map(async (path): Promise<void> => {
3738
const html = await readFile(join(srcDir, path), 'utf-8')
3839
verifyNetlifyForms(ctx, html)
3940

4041
const isFallback = fallbacks.includes(path.slice(0, -5))
42+
const isFullyStaticPage = !isFallback && fullyStaticPages.includes(path)
4143

4244
await writeFile(
4345
join(destDir, await encodeBlobKey(path)),
44-
JSON.stringify({ html, isFallback } satisfies HtmlBlob),
46+
JSON.stringify({ html, isFullyStaticPage } satisfies HtmlBlob),
4547
'utf-8',
4648
)
4749
}),

‎src/build/plugin-context.ts

Copy file name to clipboardExpand all lines: src/build/plugin-context.ts
+30Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from '@netlify/build'
1313
import type { PrerenderManifest, RoutesManifest } from 'next/dist/build/index.js'
1414
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
15+
import type { PagesManifest } from 'next/dist/build/webpack/plugins/pages-manifest-plugin.js'
1516
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
1617
import { satisfies } from 'semver'
1718

@@ -370,6 +371,35 @@ export class PluginContext {
370371
return this.#fallbacks
371372
}
372373

374+
#fullyStaticHtmlPages: string[] | null = null
375+
/**
376+
* Get an array of fully static pages router pages (no `getServerSideProps` or `getStaticProps`).
377+
* Those are being served as-is without involving CacheHandler, so we need to keep track of them
378+
* to make sure we apply permanent caching headers for responses that use them.
379+
*/
380+
async getFullyStaticHtmlPages(): Promise<string[]> {
381+
if (!this.#fullyStaticHtmlPages) {
382+
const pagesManifest = JSON.parse(
383+
await readFile(join(this.publishDir, 'server/pages-manifest.json'), 'utf-8'),
384+
) as PagesManifest
385+
386+
this.#fullyStaticHtmlPages = Object.values(pagesManifest)
387+
.filter(
388+
(filePath) =>
389+
// Limit handling to pages router files (App Router pages should not be included in pages-manifest.json
390+
// as they have their own app-paths-manifest.json)
391+
filePath.startsWith('pages/') &&
392+
// Fully static pages will have entries in the pages-manifest.json pointing to .html files.
393+
// Pages with data fetching exports will point to .js files.
394+
filePath.endsWith('.html'),
395+
)
396+
// values will be prefixed with `pages/`, so removing it here for consistency with other methods
397+
// like `getFallbacks` that return the route without the prefix
398+
.map((filePath) => relative('pages', filePath))
399+
}
400+
return this.#fullyStaticHtmlPages
401+
}
402+
373403
/** Fails a build with a message and an optional error */
374404
failBuild(message: string, error?: unknown): never {
375405
return this.utils.build.failBuild(message, error instanceof Error ? { error } : undefined)

‎src/run/next.cts

Copy file name to clipboardExpand all lines: src/run/next.cts
+30-5Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import fs from 'fs/promises'
2-
import { relative, resolve } from 'path'
1+
import { AsyncLocalStorage } from 'node:async_hooks'
2+
import fs from 'node:fs/promises'
3+
import { relative, resolve } from 'node:path'
34

45
// @ts-expect-error no types installed
56
import { patchFs } from 'fs-monkey'
@@ -79,6 +80,13 @@ ResponseCache.prototype.get = function get(...getArgs: unknown[]) {
7980
type FS = typeof import('fs')
8081

8182
export async function getMockedRequestHandler(...args: Parameters<typeof getRequestHandlers>) {
83+
const initContext = { initializingServer: true }
84+
/**
85+
* Using async local storage to identify operations happening as part of server initialization
86+
* and not part of handling of current request.
87+
*/
88+
const initAsyncLocalStorage = new AsyncLocalStorage<typeof initContext>()
89+
8290
const tracer = getTracer()
8391
return tracer.withActiveSpan('mocked request handler', async () => {
8492
const ofs = { ...fs }
@@ -96,9 +104,21 @@ export async function getMockedRequestHandler(...args: Parameters<typeof getRequ
96104
const relPath = relative(resolve('.next/server/pages'), path)
97105
const file = await cacheStore.get<HtmlBlob>(relPath, 'staticHtml.get')
98106
if (file !== null) {
99-
if (!file.isFallback) {
107+
if (file.isFullyStaticPage) {
100108
const requestContext = getRequestContext()
101-
if (requestContext) {
109+
// On server initialization Next.js attempt to preload all pages
110+
// which might result in reading .html files from the file system
111+
// for fully static pages. We don't want to capture those cases.
112+
// Note that Next.js does NOT cache read html files so on actual requests
113+
// that those will be served, it will read those AGAIN and then we do
114+
// want to capture fact of reading them.
115+
const { initializingServer } = initAsyncLocalStorage.getStore() ?? {}
116+
if (!initializingServer && requestContext) {
117+
console.log('setting usedFsReadForNonFallback to true', {
118+
requestContext,
119+
initContext: initAsyncLocalStorage.getStore(),
120+
initializingServer,
121+
})
102122
requestContext.usedFsReadForNonFallback = true
103123
}
104124
}
@@ -120,7 +140,12 @@ export async function getMockedRequestHandler(...args: Parameters<typeof getRequ
120140
require('fs').promises,
121141
)
122142

123-
const requestHandlers = await getRequestHandlers(...args)
143+
const requestHandlers = await initAsyncLocalStorage.run(initContext, async () => {
144+
// we need to await getRequestHandlers(...) promise in this callback to ensure that initAsyncLocalStorage
145+
// is available in async / background work
146+
return await getRequestHandlers(...args)
147+
})
148+
124149
// depending on Next.js version requestHandlers might be an array of object
125150
// see https://github.com/vercel/next.js/commit/08e7410f15706379994b54c3195d674909a8d533#diff-37243d614f1f5d3f7ea50bbf2af263f6b1a9a4f70e84427977781e07b02f57f1R742
126151
return Array.isArray(requestHandlers) ? requestHandlers[0] : requestHandlers.requestHandler

‎src/shared/blob-types.cts

Copy file name to clipboardExpand all lines: src/shared/blob-types.cts
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export type TagManifest = { revalidatedAt: number }
44

55
export type HtmlBlob = {
66
html: string
7-
isFallback: boolean
7+
isFullyStaticPage: boolean
88
}
99

1010
export type BlobType = NetlifyCacheHandlerValue | TagManifest | HtmlBlob
@@ -24,9 +24,9 @@ export const isHtmlBlob = (value: BlobType): value is HtmlBlob => {
2424
typeof value === 'object' &&
2525
value !== null &&
2626
'html' in value &&
27-
'isFallback' in value &&
27+
'isFullyStaticPage' in value &&
2828
typeof value.html === 'string' &&
29-
typeof value.isFallback === 'boolean' &&
29+
typeof value.isFullyStaticPage === 'boolean' &&
3030
Object.keys(value).length === 2
3131
)
3232
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.