diff --git a/packages/next/server/incremental-cache.ts b/packages/next/server/incremental-cache.ts index e12c120a6b671fe..029c457f08898dd 100644 --- a/packages/next/server/incremental-cache.ts +++ b/packages/next/server/incremental-cache.ts @@ -139,7 +139,12 @@ export class IncrementalCache { // let's check the disk for seed data if (!data) { if (this.prerenderManifest.notFoundRoutes.includes(pathname)) { - return { revalidateAfter: false, value: null } + const now = Date.now() + const revalidateAfter = this.calculateRevalidate(pathname, now) + data = { + value: null, + revalidateAfter: revalidateAfter !== false ? now : false, + } } try { diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 196d88ae3c312fc..b7917d247390270 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -827,11 +827,6 @@ export async function renderToHTML( ;(data as any).revalidate = false } - // this must come after revalidate is attached - if ((renderOpts as any).isNotFound) { - return null - } - props.pageProps = Object.assign( {}, props.pageProps, @@ -843,6 +838,11 @@ export async function renderToHTML( ;(renderOpts as any).revalidate = 'revalidate' in data ? data.revalidate : undefined ;(renderOpts as any).pageData = props + + // this must come after revalidate is added to renderOpts + if ((renderOpts as any).isNotFound) { + return null + } } if (getServerSideProps) { diff --git a/test/integration/not-found-revalidate/data.txt b/test/integration/not-found-revalidate/data.txt new file mode 100644 index 000000000000000..57db2e97867b6ae --- /dev/null +++ b/test/integration/not-found-revalidate/data.txt @@ -0,0 +1 @@ +404 \ No newline at end of file diff --git a/test/integration/not-found-revalidate/pages/404.js b/test/integration/not-found-revalidate/pages/404.js index dcacb4b18df4e43..cb670e86b596cca 100644 --- a/test/integration/not-found-revalidate/pages/404.js +++ b/test/integration/not-found-revalidate/pages/404.js @@ -8,6 +8,7 @@ export default function Page(props) { } export const getStaticProps = () => { + console.log('404 getStaticProps') return { props: { notFound: true, diff --git a/test/integration/not-found-revalidate/pages/initial-not-found/[slug].js b/test/integration/not-found-revalidate/pages/initial-not-found/[slug].js new file mode 100644 index 000000000000000..5b3c406c2b12aa4 --- /dev/null +++ b/test/integration/not-found-revalidate/pages/initial-not-found/[slug].js @@ -0,0 +1,28 @@ +import fs from 'fs' +import path from 'path' + +export async function getStaticProps() { + const data = await fs.promises.readFile( + path.join(process.cwd(), 'data.txt'), + 'utf8' + ) + + console.log('revalidate', { data }) + + return { + props: { data }, + notFound: data.trim() === '404', + revalidate: 1, + } +} + +export async function getStaticPaths() { + return { + paths: [{ params: { slug: 'first' } }], + fallback: 'blocking', + } +} + +export default function Page({ data }) { + return

{data}

+} diff --git a/test/integration/not-found-revalidate/pages/initial-not-found/index.js b/test/integration/not-found-revalidate/pages/initial-not-found/index.js new file mode 100644 index 000000000000000..665faa6eac9131f --- /dev/null +++ b/test/integration/not-found-revalidate/pages/initial-not-found/index.js @@ -0,0 +1,21 @@ +import fs from 'fs' +import path from 'path' + +export async function getStaticProps() { + const data = await fs.promises.readFile( + path.join(process.cwd(), 'data.txt'), + 'utf8' + ) + + console.log('revalidate', { data }) + + return { + props: { data }, + notFound: data.trim() === '404', + revalidate: 1, + } +} + +export default function Page({ data }) { + return

{data}

+} diff --git a/test/integration/not-found-revalidate/test/index.test.js b/test/integration/not-found-revalidate/test/index.test.js index 002c0ffa2b4b911..72d07e5d1350131 100644 --- a/test/integration/not-found-revalidate/test/index.test.js +++ b/test/integration/not-found-revalidate/test/index.test.js @@ -10,14 +10,74 @@ import { killApp, fetchViaHTTP, waitFor, + check, } from 'next-test-utils' jest.setTimeout(1000 * 60 * 2) const appDir = join(__dirname, '..') +const dataFile = join(appDir, 'data.txt') + let app let appPort const runTests = () => { + it('should revalidate page when notFund returned during build', async () => { + let res = await fetchViaHTTP(appPort, '/initial-not-found/first') + let $ = cheerio.load(await res.text()) + expect(res.status).toBe(404) + expect($('#not-found').text()).toBe('404 page') + + res = await fetchViaHTTP(appPort, '/initial-not-found/second') + $ = cheerio.load(await res.text()) + expect(res.status).toBe(404) + expect($('#not-found').text()).toBe('404 page') + + res = await fetchViaHTTP(appPort, '/initial-not-found') + $ = cheerio.load(await res.text()) + expect(res.status).toBe(404) + expect($('#not-found').text()).toBe('404 page') + + await fs.writeFile(dataFile, '200') + + // wait for revalidation period + await waitFor(1500) + await fetchViaHTTP(appPort, '/initial-not-found/first') + await fetchViaHTTP(appPort, '/initial-not-found/second') + await fetchViaHTTP(appPort, '/initial-not-found') + + // wait for revalidation to occur in background + try { + await check(async () => { + res = await fetchViaHTTP(appPort, '/initial-not-found/first') + $ = cheerio.load(await res.text()) + + return res.status === 200 && $('#data').text() === '200' + ? 'success' + : `${res.status} - ${$('#data').text()}` + }, 'success') + + await check(async () => { + res = await fetchViaHTTP(appPort, '/initial-not-found/second') + $ = cheerio.load(await res.text()) + + return res.status === 200 && $('#data').text() === '200' + ? 'success' + : `${res.status} - ${$('#data').text()}` + }, 'success') + + await check(async () => { + res = await fetchViaHTTP(appPort, '/initial-not-found') + $ = cheerio.load(await res.text()) + + return res.status === 200 && $('#data').text() === '200' + ? 'success' + : `${res.status} - ${$('#data').text()}` + }, 'success') + } finally { + await fs.writeFile(dataFile, '404') + } + }) + it('should revalidate after notFound is returned for fallback: blocking', async () => { let res = await fetchViaHTTP(appPort, '/fallback-blocking/hello') let $ = cheerio.load(await res.text()) @@ -138,9 +198,13 @@ describe('SSG notFound revalidate', () => { describe('production mode', () => { beforeAll(async () => { await fs.remove(join(appDir, '.next')) - await nextBuild(appDir) + await nextBuild(appDir, undefined, { + cwd: appDir, + }) appPort = await findPort() - app = await nextStart(appDir, appPort) + app = await nextStart(appDir, appPort, { + cwd: appDir, + }) }) afterAll(() => killApp(app))