Skip to content

Commit

Permalink
Add experimental onlyGenereated flag for unstable_revalidate (#36108)
Browse files Browse the repository at this point in the history
* Add experimental ifGenereated flag for unstable_revalidate

* Apply suggestions from code review

Co-authored-by: Steven <steven@ceriously.com>

* update ifGenerated -> onlyGenerated

* rename const as well

Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
ijjk and styfle committed Apr 13, 2022
1 parent 1d8165b commit 74dead2
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 24 deletions.
14 changes: 12 additions & 2 deletions packages/next/server/api-utils/index.ts
Expand Up @@ -71,12 +71,22 @@ export function redirect(
}

export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate'
export const PRERENDER_REVALIDATE_ONLY_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
revalidateOnlyGenerated: boolean
} {
return {
isManualRevalidate:
req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId,
revalidateOnlyGenerated:
!!req.headers[PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER],
}
}

export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
Expand Down
46 changes: 32 additions & 14 deletions 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 type { __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'
Expand Down Expand Up @@ -233,8 +236,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_onlyGenerated?: boolean
}
) => unstable_revalidate(urlPath, opts || {}, req, apiContext)

const resolver = interopDefault(resolverModule)
let wasPiped = false
Expand Down Expand Up @@ -279,6 +286,9 @@ export async function apiResolver(

async function unstable_revalidate(
urlPath: string,
opts: {
unstable_onlyGenerated?: boolean
},
req: IncomingMessage,
context: ApiContext
) {
Expand All @@ -287,12 +297,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_onlyGenerated
? {
[PRERENDER_REVALIDATE_ONLY_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 || '',
},
})
Expand All @@ -302,26 +320,26 @@ 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_onlyGenerated)
) {
throw new Error(`Invalid response ${res.status}`)
}
} else if (context.revalidate) {
const {
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_onlyGenerated)
) {
throw new Error(`Invalid response ${mockRes.statusCode}`)
}
} else {
throw new Error(
Expand Down
18 changes: 12 additions & 6 deletions 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'
Expand Down Expand Up @@ -1228,12 +1228,11 @@ export default abstract class Server {
}

let isManualRevalidate = false
let revalidateOnlyGenerated = false

if (isSSG) {
isManualRevalidate = checkIsManualRevalidate(
req,
this.renderOpts.previewProps
)
;({ isManualRevalidate, revalidateOnlyGenerated } =
checkIsManualRevalidate(req, this.renderOpts.previewProps))
}

// Compute the iSSG cache key. We use the rewroteUrl since
Expand Down Expand Up @@ -1448,6 +1447,13 @@ export default abstract class Server {
fallbackMode = 'blocking'
}

// skip manual revalidate if cache is not present and
// revalidate-if-generated is set
if (isManualRevalidate && revalidateOnlyGenerated && !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)) {
Expand Down Expand Up @@ -1545,7 +1551,7 @@ export default abstract class Server {
)

if (!cacheEntry) {
if (ssgCacheKey) {
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
Expand Down
7 changes: 6 additions & 1 deletion packages/next/shared/lib/utils.ts
Expand Up @@ -253,7 +253,12 @@ export type NextApiResponse<T = any> = ServerResponse & {
) => NextApiResponse<T>
clearPreviewData: () => NextApiResponse<T>

unstable_revalidate: (urlPath: string) => Promise<void>
unstable_revalidate: (
urlPath: string,
opts?: {
unstable_onlyGenerated?: boolean
}
) => Promise<void>
}

/**
Expand Down
73 changes: 73 additions & 0 deletions test/e2e/prerender.test.ts
Expand Up @@ -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 onlyGenerated if not generated', async () => {
const res = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/blocking-fallback/test-if-generated-1',
onlyGenerated: '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 onlyGenerated 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',
onlyGenerated: '1',
},
{ redirect: 'manual' }
)

expect(res3.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,
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/prerender/pages/api/manual-revalidate.js
Expand Up @@ -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_onlyGenerated: !!req.query.onlyGenerated,
})
revalidated = true
} catch (err) {
console.error(err)
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/prerender/pages/blocking-fallback/[slug].js
Expand Up @@ -28,6 +28,8 @@ export async function getStaticProps({ params }) {

await new Promise((resolve) => setTimeout(resolve, 1000))

console.log(`getStaticProps ${params.slug}`)

return {
props: {
params,
Expand Down

0 comments on commit 74dead2

Please sign in to comment.