Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental onlyGenereated flag for unstable_revalidate #36108

Merged
merged 5 commits into from Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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_IF_GENERATED_HEADER =
'x-prerender-revalidate-if-generated'
ijjk marked this conversation as resolved.
Show resolved Hide resolved

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_IF_GENERATED_HEADER],
}
}

export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
Expand Down
43 changes: 29 additions & 14 deletions 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'
Expand Down Expand Up @@ -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_onlyGenerated?: boolean
}
) => unstable_revalidate(urlPath, opts || {}, req, apiContext)

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

async function unstable_revalidate(
urlPath: string,
opts: {
unstable_onlyGenerated?: boolean
},
req: IncomingMessage,
context: ApiContext
) {
Expand All @@ -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_onlyGenerated
? {
[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 || '',
},
})
Expand All @@ -302,26 +317,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