diff --git a/packages/next/server/api-utils/node.ts b/packages/next/server/api-utils/node.ts index 41e1229d5912..73f4d6d8cd77 100644 --- a/packages/next/server/api-utils/node.ts +++ b/packages/next/server/api-utils/node.ts @@ -315,7 +315,13 @@ async function unstable_revalidate( }, }) - if (!res.ok) { + // we use the cache header to determine successful revalidate as + // a non-200 status code can be returned from a successful revalidate + // e.g. notFound: true returns 404 status code but is successful + const cacheHeader = + res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache') + + if (cacheHeader?.toUpperCase() !== 'REVALIDATED') { throw new Error(`Invalid response ${res.status}`) } } catch (err) { diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 1a941cfa2ad0..9a6e81bf2590 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1528,6 +1528,21 @@ export default abstract class Server { return null } + if (isSSG) { + // set x-nextjs-cache header to match the header + // we set for the image-optimizer + res.setHeader( + 'x-nextjs-cache', + isManualRevalidate + ? 'REVALIDATED' + : cacheEntry.isMiss + ? 'MISS' + : cacheEntry.isStale + ? 'STALE' + : 'HIT' + ) + } + const { revalidate, value: cachedData } = cacheEntry const revalidateOptions: any = typeof revalidate !== 'undefined' && diff --git a/test/e2e/prerender.test.ts b/test/e2e/prerender.test.ts index 0f4270338020..a519236ab4c8 100644 --- a/test/e2e/prerender.test.ts +++ b/test/e2e/prerender.test.ts @@ -149,6 +149,11 @@ describe('Prerender', () => { initialRevalidateSeconds: false, srcRoute: '/api-docs/[...slug]', }, + '/blocking-fallback-once/404-on-manual-revalidate': { + dataRoute: `/_next/data/${next.buildId}/blocking-fallback-once/404-on-manual-revalidate.json`, + initialRevalidateSeconds: false, + srcRoute: '/blocking-fallback-once/[slug]', + }, '/blocking-fallback-some/a': { dataRoute: `/_next/data/${next.buildId}/blocking-fallback-some/a.json`, initialRevalidateSeconds: 1, @@ -1901,24 +1906,28 @@ describe('Prerender', () => { if (!(global as any).isNextDev) { it('should handle manual revalidate for fallback: blocking', async () => { - const html = await renderViaHTTP( + const res = await fetchViaHTTP( next.url, '/blocking-fallback/test-manual-1' ) + const html = await res.text() const $ = cheerio.load(html) const initialTime = $('#time').text() + expect(res.headers.get('x-nextjs-cache')).toMatch(/MISS/) expect($('p').text()).toMatch(/Post:.*?test-manual-1/) - const html2 = await renderViaHTTP( + const res2 = await fetchViaHTTP( next.url, '/blocking-fallback/test-manual-1' ) + const html2 = await res2.text() const $2 = cheerio.load(html2) + expect(res2.headers.get('x-nextjs-cache')).toMatch(/(HIT|STALE)/) expect(initialTime).toBe($2('#time').text()) - const res = await fetchViaHTTP( + const res3 = await fetchViaHTTP( next.url, '/api/manual-revalidate', { @@ -1927,16 +1936,18 @@ describe('Prerender', () => { { redirect: 'manual' } ) - expect(res.status).toBe(200) - const revalidateData = await res.json() + expect(res2.status).toBe(200) + const revalidateData = await res3.json() expect(revalidateData.revalidated).toBe(true) - const html4 = await renderViaHTTP( + const res4 = await fetchViaHTTP( next.url, '/blocking-fallback/test-manual-1' ) + const html4 = await res4.text() const $4 = cheerio.load(html4) expect($4('#time').text()).not.toBe(initialTime) + expect(res4.headers.get('x-nextjs-cache')).toMatch(/(HIT|STALE)/) }) it('should manual revalidate for revalidate: false', async () => { @@ -1978,6 +1989,47 @@ describe('Prerender', () => { expect($4('#time').text()).not.toBe(initialTime) }) + it('should manual revalidate that returns notFound: true', async () => { + const res = await fetchViaHTTP( + next.url, + '/blocking-fallback-once/404-on-manual-revalidate' + ) + const html = await res.text() + const $ = cheerio.load(html) + const initialTime = $('#time').text() + expect(res.headers.get('x-nextjs-cache')).toBe('HIT') + + expect($('p').text()).toMatch(/Post:.*?404-on-manual-revalidate/) + + const html2 = await renderViaHTTP( + next.url, + '/blocking-fallback-once/404-on-manual-revalidate' + ) + const $2 = cheerio.load(html2) + + expect(initialTime).toBe($2('#time').text()) + + const res2 = await fetchViaHTTP( + next.url, + '/api/manual-revalidate', + { + pathname: '/blocking-fallback-once/404-on-manual-revalidate', + }, + { redirect: 'manual' } + ) + expect(res2.status).toBe(200) + const revalidateData = await res2.json() + expect(revalidateData.revalidated).toBe(true) + + const res3 = await fetchViaHTTP( + next.url, + '/blocking-fallback-once/404-on-manual-revalidate' + ) + expect(res3.status).toBe(404) + expect(await res3.text()).toContain('This page could not be found') + expect(res3.headers.get('x-nextjs-cache')).toBe('HIT') + }) + it('should handle manual revalidate for fallback: false', async () => { const res = await fetchViaHTTP( next.url, diff --git a/test/e2e/prerender/pages/blocking-fallback-once/[slug].js b/test/e2e/prerender/pages/blocking-fallback-once/[slug].js index 4ca33a678ebe..2ef830265aad 100644 --- a/test/e2e/prerender/pages/blocking-fallback-once/[slug].js +++ b/test/e2e/prerender/pages/blocking-fallback-once/[slug].js @@ -4,7 +4,11 @@ import { useRouter } from 'next/router' export async function getStaticPaths() { return { - paths: [], + paths: [ + { + params: { slug: '404-on-manual-revalidate' }, + }, + ], fallback: 'blocking', } } @@ -12,6 +16,14 @@ export async function getStaticPaths() { export async function getStaticProps({ params }) { await new Promise((resolve) => setTimeout(resolve, 1000)) + if (process.env.NEXT_PHASE !== 'phase-production-build') { + if (params.slug === '404-on-manual-revalidate') { + return { + notFound: true, + } + } + } + return { props: { params,