Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions src/build/content/prerendered.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const routeToFilePath = (path: string) => {

const buildPagesCacheValue = async (
path: string,
initialRevalidateSeconds: number | false | undefined,
shouldUseEnumKind: boolean,
shouldSkipJson = false,
): Promise<NetlifyCachedPageValue> => ({
Expand All @@ -65,6 +66,7 @@ const buildPagesCacheValue = async (
pageData: shouldSkipJson ? {} : JSON.parse(await readFile(`${path}.json`, 'utf-8')),
headers: undefined,
status: undefined,
revalidate: initialRevalidateSeconds,
})

const buildAppCacheValue = async (
Expand Down Expand Up @@ -178,6 +180,7 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
}
value = await buildPagesCacheValue(
join(ctx.publishDir, 'server/pages', key),
meta.initialRevalidateSeconds,
shouldUseEnumKind,
)
break
Expand Down Expand Up @@ -210,6 +213,7 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
const key = routeToFilePath(route)
const value = await buildPagesCacheValue(
join(ctx.publishDir, 'server/pages', key),
undefined,
shouldUseEnumKind,
true, // there is no corresponding json file for fallback, so we are skipping it for this entry
)
Expand Down
5 changes: 5 additions & 0 deletions src/run/handlers/cache.cts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
case 'PAGES': {
const { revalidate, ...restOfPageValue } = blob.value

const requestContext = getRequestContext()
if (requestContext) {
requestContext.pageHandlerRevalidate = revalidate
}

span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl })

await this.injectEntryToPrerenderManifest(key, revalidate)
Expand Down
1 change: 1 addition & 0 deletions src/run/handlers/request-context.cts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type RequestContext = {
didPagesRouterOnDemandRevalidate?: boolean
serverTiming?: string
routeHandlerRevalidate?: NetlifyCachedRouteValue['revalidate']
pageHandlerRevalidate?: NetlifyCachedRouteValue['revalidate']
/**
* Track promise running in the background and need to be waited for.
* Uses `context.waitUntil` if available, otherwise stores promises to
Expand Down
47 changes: 34 additions & 13 deletions src/run/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Span } from '@opentelemetry/api'
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'

import { encodeBlobKey } from '../shared/blobkey.js'
import type { NetlifyCachedRouteValue } from '../shared/cache-types.cjs'

import { getLogger, RequestContext } from './handlers/request-context.cjs'
import type { RuntimeTracer } from './handlers/tracer.cjs'
Expand Down Expand Up @@ -197,6 +198,19 @@ export const adjustDateHeader = async ({
headers.set('date', lastModifiedDate.toUTCString())
}

function setCacheControlFromRequestContext(
headers: Headers,
revalidate: NetlifyCachedRouteValue['revalidate'],
) {
const cdnCacheControl =
// if we are serving already stale response, instruct edge to not attempt to cache that response
headers.get('x-nextjs-cache') === 'STALE'
? 'public, max-age=0, must-revalidate, durable'
: `s-maxage=${revalidate || 31536000}, stale-while-revalidate=31536000, durable`

headers.set('netlify-cdn-cache-control', cdnCacheControl)
}

/**
* Ensure stale-while-revalidate and s-maxage don't leak to the client, but
* assume the user knows what they are doing if CDN cache controls are set
Expand All @@ -214,13 +228,7 @@ export const setCacheControlHeaders = (
!headers.has('netlify-cdn-cache-control')
) {
// handle CDN Cache Control on Route Handler responses
const cdnCacheControl =
// if we are serving already stale response, instruct edge to not attempt to cache that response
headers.get('x-nextjs-cache') === 'STALE'
? 'public, max-age=0, must-revalidate, durable'
: `s-maxage=${requestContext.routeHandlerRevalidate === false ? 31536000 : requestContext.routeHandlerRevalidate}, stale-while-revalidate=31536000, durable`

headers.set('netlify-cdn-cache-control', cdnCacheControl)
setCacheControlFromRequestContext(headers, requestContext.routeHandlerRevalidate)
return
}

Expand All @@ -231,14 +239,27 @@ export const setCacheControlHeaders = (
.log('NetlifyHeadersHandler.trailingSlashRedirect')
}

if (status === 404 && request.url.endsWith('.php')) {
// temporary CDN Cache Control handling for bot probes on PHP files
// https://linear.app/netlify/issue/FRB-1344/prevent-excessive-ssr-invocations-due-to-404-routes
headers.set('cache-control', 'public, max-age=0, must-revalidate')
headers.set('netlify-cdn-cache-control', `max-age=31536000, durable`)
const cacheControl = headers.get('cache-control')
if (status === 404) {
if (request.url.endsWith('.php')) {
// temporary CDN Cache Control handling for bot probes on PHP files
// https://linear.app/netlify/issue/FRB-1344/prevent-excessive-ssr-invocations-due-to-404-routes
headers.set('cache-control', 'public, max-age=0, must-revalidate')
headers.set('netlify-cdn-cache-control', `max-age=31536000, durable`)
return
}

if (
process.env.CACHE_404_PAGE &&
request.url.endsWith('/404') &&
['GET', 'HEAD'].includes(request.method)
) {
// handle CDN Cache Control on 404 Page responses
setCacheControlFromRequestContext(headers, requestContext.pageHandlerRevalidate)
return
}
}

const cacheControl = headers.get('cache-control')
if (
cacheControl !== null &&
['GET', 'HEAD'].includes(request.method) &&
Expand Down
10 changes: 5 additions & 5 deletions tests/e2e/page-router.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { test } from '../utils/playwright-helpers.js'
import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'
import { test } from '../utils/playwright-helpers.js'

export function waitFor(millis: number) {
return new Promise((resolve) => setTimeout(resolve, millis))
Expand Down Expand Up @@ -462,7 +462,7 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
const headers = response?.headers() || {}
expect(response?.status()).toBe(404)

expect(await page.textContent('h1')).toBe('404')
expect(await page.textContent('p')).toBe('Custom 404 page')

// https://github.com/vercel/next.js/pull/69802 made changes to returned cache-control header,
// after that (14.2.10 and canary.147) 404 pages would have `private` directive, before that
Expand All @@ -486,7 +486,7 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
const headers = response?.headers() || {}
expect(response?.status()).toBe(404)

expect(await page.textContent('h1')).toBe('404')
expect(await page.textContent('p')).toBe('Custom 404 page')

expect(headers['netlify-cdn-cache-control']).toBe(
nextVersionSatisfies('>=15.0.0-canary.187')
Expand Down Expand Up @@ -1326,7 +1326,7 @@ test.describe('Page Router with basePath and i18n', () => {
const headers = response?.headers() || {}
expect(response?.status()).toBe(404)

expect(await page.textContent('h1')).toBe('404')
expect(await page.textContent('p')).toBe('Custom 404 page')

// https://github.com/vercel/next.js/pull/69802 made changes to returned cache-control header,
// after that 404 pages would have `private` directive, before that it would not
Expand All @@ -1349,7 +1349,7 @@ test.describe('Page Router with basePath and i18n', () => {
const headers = response?.headers() || {}
expect(response?.status()).toBe(404)

expect(await page.textContent('h1')).toBe('404')
expect(await page.textContent('p')).toBe('Custom 404 page')

expect(headers['netlify-cdn-cache-control']).toBe(
nextVersionSatisfies('>=15.0.0-canary.187')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
},
generateBuildId: () => 'build-id',
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "page-router-404-get-static-props-with-revalidate",
"version": "0.1.0",
"private": true,
"scripts": {
"postinstall": "next build",
"dev": "next dev",
"build": "next build"
},
"dependencies": {
"next": "latest",
"react": "18.2.0",
"react-dom": "18.2.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default function NotFound({ timestamp }) {
return (
<p>
Custom 404 page with revalidate: <pre data-testid="timestamp">{timestamp}</pre>
</p>
)
}

/** @type {import('next').GetStaticProps} */
export const getStaticProps = ({ locale }) => {
return {
props: {
timestamp: Date.now(),
},
revalidate: 300,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const Product = ({ time, slug }) => (
<div>
<h1>Product {slug}</h1>
<p>
This page uses getStaticProps() and getStaticPaths() to pre-fetch a Product
<span data-testid="date-now">{time}</span>
</p>
</div>
)

/** @type {import('next').GetStaticProps} */
export async function getStaticProps({ params }) {
if (params.slug === 'not-found-no-revalidate') {
return {
notFound: true,
}
} else if (params.slug === 'not-found-with-revalidate') {
return {
notFound: true,
revalidate: 600,
}
}

return {
props: {
time: new Date().toISOString(),
slug: params.slug,
},
}
}

/** @type {import('next').GetStaticPaths} */
export const getStaticPaths = () => {
return {
paths: [],
fallback: 'blocking', // false or "blocking"
}
}

export default Product
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ const Product = ({ time, slug }) => (
</div>
)

/** @type {import('next').GetStaticProps} */
export async function getStaticProps({ params }) {
if (params.slug === 'not-found-no-revalidate') {
return {
notFound: true,
}
} else if (params.slug === 'not-found-with-revalidate') {
return {
notFound: true,
revalidate: 600,
}
}

return {
props: {
time: new Date().toISOString(),
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/page-router/pages/404.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function NotFound() {
return <p>Custom 404 page</p>
}
7 changes: 7 additions & 0 deletions tests/fixtures/page-router/pages/products/[slug].js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const Product = ({ time, slug }) => (
)

export async function getStaticProps({ params }) {
if (params.slug === 'not-found-with-revalidate') {
return {
notFound: true,
revalidate: 600,
}
}

return {
props: {
time: new Date().toISOString(),
Expand Down
Loading
Loading