From 9f0bed1ff816fa84c0c9f4dbf91680aa58bdb639 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 12 Apr 2022 17:08:42 -0500 Subject: [PATCH 1/4] Add experimental ifGenereated flag for unstable_revalidate --- packages/next/server/api-utils/index.ts | 14 +++- packages/next/server/api-utils/node.ts | 43 +++++++---- packages/next/server/base-server.ts | 16 +++- packages/next/shared/lib/utils.ts | 7 +- test/e2e/prerender.test.ts | 73 +++++++++++++++++++ .../prerender/pages/api/manual-revalidate.js | 4 +- .../pages/blocking-fallback/[slug].js | 2 + 7 files changed, 137 insertions(+), 22 deletions(-) diff --git a/packages/next/server/api-utils/index.ts b/packages/next/server/api-utils/index.ts index 59c6a9bc0dfc..25cc04057f69 100644 --- a/packages/next/server/api-utils/index.ts +++ b/packages/next/server/api-utils/index.ts @@ -71,12 +71,22 @@ export function redirect( } export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate' +export const PRERENDER_REVALIDATE_IF_GENERATED_HEADER = + 'x-prerender-revalidate-if-generated' export function checkIsManualRevalidate( req: IncomingMessage | BaseNextRequest, previewProps: __ApiPreviewProps -): boolean { - return req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId +): { + isManualRevalidate: boolean + revalidateIfGenerated: boolean +} { + return { + isManualRevalidate: + req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId, + revalidateIfGenerated: + !!req.headers[PRERENDER_REVALIDATE_IF_GENERATED_HEADER], + } } export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass` diff --git a/packages/next/server/api-utils/node.ts b/packages/next/server/api-utils/node.ts index 68f928a23755..b5ea96f736f4 100644 --- a/packages/next/server/api-utils/node.ts +++ b/packages/next/server/api-utils/node.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'http' import type { NextApiRequest, NextApiResponse } from '../../shared/lib/utils' import type { PageConfig } from 'next/types' -import type { __ApiPreviewProps } from '.' +import { PRERENDER_REVALIDATE_IF_GENERATED_HEADER, __ApiPreviewProps } from '.' import type { BaseNextRequest, BaseNextResponse } from '../base-http' import type { CookieSerializeOptions } from 'next/dist/compiled/cookie' import type { PreviewData } from 'next/types' @@ -233,8 +233,12 @@ export async function apiResolver( apiRes.setPreviewData = (data, options = {}) => setPreviewData(apiRes, data, Object.assign({}, apiContext, options)) apiRes.clearPreviewData = () => clearPreviewData(apiRes) - apiRes.unstable_revalidate = (urlPath: string) => - unstable_revalidate(urlPath, req, apiContext) + apiRes.unstable_revalidate = ( + urlPath: string, + opts?: { + unstable_ifGenerated?: boolean + } + ) => unstable_revalidate(urlPath, opts || {}, req, apiContext) const resolver = interopDefault(resolverModule) let wasPiped = false @@ -279,6 +283,9 @@ export async function apiResolver( async function unstable_revalidate( urlPath: string, + opts: { + unstable_ifGenerated?: boolean + }, req: IncomingMessage, context: ApiContext ) { @@ -287,12 +294,20 @@ async function unstable_revalidate( `Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received ${urlPath}` ) } + const revalidateHeaders = { + [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, + ...(opts.unstable_ifGenerated + ? { + [PRERENDER_REVALIDATE_IF_GENERATED_HEADER]: '1', + } + : {}), + } try { if (context.trustHostHeader) { const res = await fetch(`https://${req.headers.host}${urlPath}`, { headers: { - [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, + ...revalidateHeaders, cookie: req.headers.cookie || '', }, }) @@ -302,7 +317,10 @@ async function unstable_revalidate( const cacheHeader = res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache') - if (cacheHeader?.toUpperCase() !== 'REVALIDATED') { + if ( + cacheHeader?.toUpperCase() !== 'REVALIDATED' && + !(res.status === 404 && opts.unstable_ifGenerated) + ) { throw new Error(`Invalid response ${res.status}`) } } else if (context.revalidate) { @@ -310,18 +328,15 @@ async function unstable_revalidate( req: mockReq, res: mockRes, streamPromise, - } = mockRequest( - urlPath, - { - [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, - }, - 'GET' - ) + } = mockRequest(urlPath, revalidateHeaders, 'GET') await context.revalidate(mockReq, mockRes) await streamPromise - if (mockRes.getHeader('x-nextjs-cache') !== 'REVALIDATED') { - throw new Error(`Invalid response ${mockRes.status}`) + if ( + mockRes.getHeader('x-nextjs-cache') !== 'REVALIDATED' && + !(mockRes.statusCode === 404 && opts.unstable_ifGenerated) + ) { + throw new Error(`Invalid response ${mockRes.statusCode}`) } } else { throw new Error( diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 7d439aa7b61b..bcba72badbba 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1,4 +1,4 @@ -import type { __ApiPreviewProps } from './api-utils' +import { __ApiPreviewProps } from './api-utils' import type { CustomRoutes } from '../lib/load-custom-routes' import type { DomainLocale } from './config' import type { DynamicRoutes, PageChecker, Params, Route } from './router' @@ -1228,12 +1228,13 @@ export default abstract class Server { } let isManualRevalidate = false + let revalidateIfGenerated = false if (isSSG) { - isManualRevalidate = checkIsManualRevalidate( + ;({ isManualRevalidate, revalidateIfGenerated } = checkIsManualRevalidate( req, this.renderOpts.previewProps - ) + )) } // Compute the iSSG cache key. We use the rewroteUrl since @@ -1448,6 +1449,13 @@ export default abstract class Server { fallbackMode = 'blocking' } + // skip manual revalidate if cache is not present and + // revalidate-if-generated is set + if (isManualRevalidate && revalidateIfGenerated && !hadCache) { + await this.render404(req, res) + return null + } + // only allow manual revalidate for fallback: true/blocking // or for prerendered fallback: false paths if (isManualRevalidate && (fallbackMode !== false || hadCache)) { @@ -1545,7 +1553,7 @@ export default abstract class Server { ) if (!cacheEntry) { - if (ssgCacheKey) { + if (ssgCacheKey && !(isManualRevalidate && revalidateIfGenerated)) { // A cache entry might not be generated if a response is written // in `getInitialProps` or `getServerSideProps`, but those shouldn't // have a cache key. If we do have a cache key but we don't end up diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 3d21f021804e..7a32e4d144bb 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -253,7 +253,12 @@ export type NextApiResponse = ServerResponse & { ) => NextApiResponse clearPreviewData: () => NextApiResponse - unstable_revalidate: (urlPath: string) => Promise + unstable_revalidate: ( + urlPath: string, + opts?: { + unstable_ifGenerated?: boolean + } + ) => Promise } /** diff --git a/test/e2e/prerender.test.ts b/test/e2e/prerender.test.ts index 018fe597589d..6cd4a33230f2 100644 --- a/test/e2e/prerender.test.ts +++ b/test/e2e/prerender.test.ts @@ -2008,6 +2008,79 @@ describe('Prerender', () => { expect(res4.headers.get('x-nextjs-cache')).toMatch(/(HIT|STALE)/) }) + it('should not manual revalidate for fallback: blocking with ifGenerated if not generated', async () => { + const res = await fetchViaHTTP( + next.url, + '/api/manual-revalidate', + { + pathname: '/blocking-fallback/test-if-generated-1', + ifGenerated: '1', + }, + { redirect: 'manual' } + ) + + expect(res.status).toBe(200) + const revalidateData = await res.json() + expect(revalidateData.revalidated).toBe(true) + + expect(next.cliOutput).not.toContain( + `getStaticProps test-if-generated-1` + ) + + const res2 = await fetchViaHTTP( + next.url, + '/blocking-fallback/test-if-generated-1' + ) + expect(res2.headers.get('x-nextjs-cache')).toMatch(/(MISS)/) + expect(next.cliOutput).toContain(`getStaticProps test-if-generated-1`) + }) + + it('should manual revalidate for fallback: blocking with ifGenerated if generated', async () => { + const res = await fetchViaHTTP( + next.url, + '/blocking-fallback/test-if-generated-2' + ) + 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-if-generated-2/) + + const res2 = await fetchViaHTTP( + next.url, + '/blocking-fallback/test-if-generated-2' + ) + 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 res3 = await fetchViaHTTP( + next.url, + '/api/manual-revalidate', + { + pathname: '/blocking-fallback/test-if-generated-2', + ifGenerated: '1', + }, + { redirect: 'manual' } + ) + + expect(res2.status).toBe(200) + const revalidateData = await res3.json() + expect(revalidateData.revalidated).toBe(true) + + const res4 = await fetchViaHTTP( + next.url, + '/blocking-fallback/test-if-generated-2' + ) + 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 () => { const html = await renderViaHTTP( next.url, diff --git a/test/e2e/prerender/pages/api/manual-revalidate.js b/test/e2e/prerender/pages/api/manual-revalidate.js index dc88de2b8497..076383029fe4 100644 --- a/test/e2e/prerender/pages/api/manual-revalidate.js +++ b/test/e2e/prerender/pages/api/manual-revalidate.js @@ -3,7 +3,9 @@ export default async function handler(req, res) { // make sure to use trusted value for revalidating let revalidated = false try { - await res.unstable_revalidate(req.query.pathname) + await res.unstable_revalidate(req.query.pathname, { + unstable_ifGenerated: !!req.query.ifGenerated, + }) revalidated = true } catch (err) { console.error(err) diff --git a/test/e2e/prerender/pages/blocking-fallback/[slug].js b/test/e2e/prerender/pages/blocking-fallback/[slug].js index 5665ae6f008d..cf6aec867a32 100644 --- a/test/e2e/prerender/pages/blocking-fallback/[slug].js +++ b/test/e2e/prerender/pages/blocking-fallback/[slug].js @@ -28,6 +28,8 @@ export async function getStaticProps({ params }) { await new Promise((resolve) => setTimeout(resolve, 1000)) + console.log(`getStaticProps ${params.slug}`) + return { props: { params, From e12af862b3dce40787475d1b6ac09acd489fed39 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 13 Apr 2022 10:51:30 -0500 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Steven --- test/e2e/prerender.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/prerender.test.ts b/test/e2e/prerender.test.ts index 6cd4a33230f2..4fe11ad886eb 100644 --- a/test/e2e/prerender.test.ts +++ b/test/e2e/prerender.test.ts @@ -2067,7 +2067,7 @@ describe('Prerender', () => { { redirect: 'manual' } ) - expect(res2.status).toBe(200) + expect(res3.status).toBe(200) const revalidateData = await res3.json() expect(revalidateData.revalidated).toBe(true) From 1d88fc9ac669b57e2aea7a2434ac1d927f561bab Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 13 Apr 2022 11:03:36 -0500 Subject: [PATCH 3/4] update ifGenerated -> onlyGenerated --- packages/next/server/api-utils/index.ts | 4 ++-- packages/next/server/api-utils/node.ts | 10 +++++----- packages/next/server/base-server.ts | 12 +++++------- packages/next/shared/lib/utils.ts | 2 +- test/e2e/prerender.test.ts | 8 ++++---- test/e2e/prerender/pages/api/manual-revalidate.js | 2 +- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/next/server/api-utils/index.ts b/packages/next/server/api-utils/index.ts index 25cc04057f69..1bab761d9b94 100644 --- a/packages/next/server/api-utils/index.ts +++ b/packages/next/server/api-utils/index.ts @@ -79,12 +79,12 @@ export function checkIsManualRevalidate( previewProps: __ApiPreviewProps ): { isManualRevalidate: boolean - revalidateIfGenerated: boolean + revalidateOnlyGenerated: boolean } { return { isManualRevalidate: req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId, - revalidateIfGenerated: + revalidateOnlyGenerated: !!req.headers[PRERENDER_REVALIDATE_IF_GENERATED_HEADER], } } diff --git a/packages/next/server/api-utils/node.ts b/packages/next/server/api-utils/node.ts index b5ea96f736f4..745a3044f54a 100644 --- a/packages/next/server/api-utils/node.ts +++ b/packages/next/server/api-utils/node.ts @@ -236,7 +236,7 @@ export async function apiResolver( apiRes.unstable_revalidate = ( urlPath: string, opts?: { - unstable_ifGenerated?: boolean + unstable_onlyGenerated?: boolean } ) => unstable_revalidate(urlPath, opts || {}, req, apiContext) @@ -284,7 +284,7 @@ export async function apiResolver( async function unstable_revalidate( urlPath: string, opts: { - unstable_ifGenerated?: boolean + unstable_onlyGenerated?: boolean }, req: IncomingMessage, context: ApiContext @@ -296,7 +296,7 @@ async function unstable_revalidate( } const revalidateHeaders = { [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, - ...(opts.unstable_ifGenerated + ...(opts.unstable_onlyGenerated ? { [PRERENDER_REVALIDATE_IF_GENERATED_HEADER]: '1', } @@ -319,7 +319,7 @@ async function unstable_revalidate( if ( cacheHeader?.toUpperCase() !== 'REVALIDATED' && - !(res.status === 404 && opts.unstable_ifGenerated) + !(res.status === 404 && opts.unstable_onlyGenerated) ) { throw new Error(`Invalid response ${res.status}`) } @@ -334,7 +334,7 @@ async function unstable_revalidate( if ( mockRes.getHeader('x-nextjs-cache') !== 'REVALIDATED' && - !(mockRes.statusCode === 404 && opts.unstable_ifGenerated) + !(mockRes.statusCode === 404 && opts.unstable_onlyGenerated) ) { throw new Error(`Invalid response ${mockRes.statusCode}`) } diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index bcba72badbba..94d7ae19d9f5 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1228,13 +1228,11 @@ export default abstract class Server { } let isManualRevalidate = false - let revalidateIfGenerated = false + let revalidateOnlyGenerated = false if (isSSG) { - ;({ isManualRevalidate, revalidateIfGenerated } = checkIsManualRevalidate( - req, - this.renderOpts.previewProps - )) + ;({ isManualRevalidate, revalidateOnlyGenerated } = + checkIsManualRevalidate(req, this.renderOpts.previewProps)) } // Compute the iSSG cache key. We use the rewroteUrl since @@ -1451,7 +1449,7 @@ export default abstract class Server { // skip manual revalidate if cache is not present and // revalidate-if-generated is set - if (isManualRevalidate && revalidateIfGenerated && !hadCache) { + if (isManualRevalidate && revalidateOnlyGenerated && !hadCache) { await this.render404(req, res) return null } @@ -1553,7 +1551,7 @@ export default abstract class Server { ) if (!cacheEntry) { - if (ssgCacheKey && !(isManualRevalidate && revalidateIfGenerated)) { + if (ssgCacheKey && !(isManualRevalidate && revalidateOnlyGenerated)) { // A cache entry might not be generated if a response is written // in `getInitialProps` or `getServerSideProps`, but those shouldn't // have a cache key. If we do have a cache key but we don't end up diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 7a32e4d144bb..8bafaf018789 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -256,7 +256,7 @@ export type NextApiResponse = ServerResponse & { unstable_revalidate: ( urlPath: string, opts?: { - unstable_ifGenerated?: boolean + unstable_onlyGenerated?: boolean } ) => Promise } diff --git a/test/e2e/prerender.test.ts b/test/e2e/prerender.test.ts index 4fe11ad886eb..b1514a54b971 100644 --- a/test/e2e/prerender.test.ts +++ b/test/e2e/prerender.test.ts @@ -2008,13 +2008,13 @@ describe('Prerender', () => { expect(res4.headers.get('x-nextjs-cache')).toMatch(/(HIT|STALE)/) }) - it('should not manual revalidate for fallback: blocking with ifGenerated if not generated', async () => { + it('should not manual revalidate for fallback: blocking with onlyGenerated if not generated', async () => { const res = await fetchViaHTTP( next.url, '/api/manual-revalidate', { pathname: '/blocking-fallback/test-if-generated-1', - ifGenerated: '1', + onlyGenerated: '1', }, { redirect: 'manual' } ) @@ -2035,7 +2035,7 @@ describe('Prerender', () => { expect(next.cliOutput).toContain(`getStaticProps test-if-generated-1`) }) - it('should manual revalidate for fallback: blocking with ifGenerated if generated', async () => { + it('should manual revalidate for fallback: blocking with onlyGenerated if generated', async () => { const res = await fetchViaHTTP( next.url, '/blocking-fallback/test-if-generated-2' @@ -2062,7 +2062,7 @@ describe('Prerender', () => { '/api/manual-revalidate', { pathname: '/blocking-fallback/test-if-generated-2', - ifGenerated: '1', + onlyGenerated: '1', }, { redirect: 'manual' } ) diff --git a/test/e2e/prerender/pages/api/manual-revalidate.js b/test/e2e/prerender/pages/api/manual-revalidate.js index 076383029fe4..e87bf54a26eb 100644 --- a/test/e2e/prerender/pages/api/manual-revalidate.js +++ b/test/e2e/prerender/pages/api/manual-revalidate.js @@ -4,7 +4,7 @@ export default async function handler(req, res) { let revalidated = false try { await res.unstable_revalidate(req.query.pathname, { - unstable_ifGenerated: !!req.query.ifGenerated, + unstable_onlyGenerated: !!req.query.onlyGenerated, }) revalidated = true } catch (err) { From d62d9e6e12f406acf1df1663886d055352c6598e Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 13 Apr 2022 11:20:45 -0500 Subject: [PATCH 4/4] rename const as well --- packages/next/server/api-utils/index.ts | 4 ++-- packages/next/server/api-utils/node.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/next/server/api-utils/index.ts b/packages/next/server/api-utils/index.ts index 1bab761d9b94..abb8b396f67d 100644 --- a/packages/next/server/api-utils/index.ts +++ b/packages/next/server/api-utils/index.ts @@ -71,7 +71,7 @@ export function redirect( } export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate' -export const PRERENDER_REVALIDATE_IF_GENERATED_HEADER = +export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = 'x-prerender-revalidate-if-generated' export function checkIsManualRevalidate( @@ -85,7 +85,7 @@ export function checkIsManualRevalidate( isManualRevalidate: req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId, revalidateOnlyGenerated: - !!req.headers[PRERENDER_REVALIDATE_IF_GENERATED_HEADER], + !!req.headers[PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER], } } diff --git a/packages/next/server/api-utils/node.ts b/packages/next/server/api-utils/node.ts index 745a3044f54a..310c0afd12f4 100644 --- a/packages/next/server/api-utils/node.ts +++ b/packages/next/server/api-utils/node.ts @@ -1,7 +1,10 @@ import type { IncomingMessage, ServerResponse } from 'http' import type { NextApiRequest, NextApiResponse } from '../../shared/lib/utils' import type { PageConfig } from 'next/types' -import { PRERENDER_REVALIDATE_IF_GENERATED_HEADER, __ApiPreviewProps } from '.' +import { + PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, + __ApiPreviewProps, +} from '.' import type { BaseNextRequest, BaseNextResponse } from '../base-http' import type { CookieSerializeOptions } from 'next/dist/compiled/cookie' import type { PreviewData } from 'next/types' @@ -298,7 +301,7 @@ async function unstable_revalidate( [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, ...(opts.unstable_onlyGenerated ? { - [PRERENDER_REVALIDATE_IF_GENERATED_HEADER]: '1', + [PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER]: '1', } : {}), }