Skip to content

Commit

Permalink
Fix cross-worker revalidate API (#49101)
Browse files Browse the repository at this point in the history
Currently we invoke the revalidate request directly in the current
server when `res.revalidate()` is called. However app needs to be
rendered in a separate worker so this results in an error of React. This
PR fixes it by sending the request via IPC so the main process will
delegate that to the correct render worker.

Closes #48948.

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed May 3, 2023
1 parent e54e38a commit 931c101
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 18 deletions.
57 changes: 44 additions & 13 deletions packages/next/src/server/api-utils/node.ts
Expand Up @@ -27,7 +27,6 @@ import {
SYMBOL_PREVIEW_DATA,
RESPONSE_LIMIT_DEFAULT,
} from './index'
import { createRequestResponseMocks } from '../lib/mock-request'
import { getTracer } from '../lib/trace/tracer'
import { NodeSpan } from '../lib/trace/constants'
import { RequestCookies } from '../web/spec-extension/cookies'
Expand All @@ -36,6 +35,7 @@ import {
PRERENDER_REVALIDATE_HEADER,
PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER,
} from '../../lib/constants'
import { invokeRequest } from '../lib/server-ipc'

export function tryGetPreviewData(
req: IncomingMessage | BaseNextRequest | Request,
Expand Down Expand Up @@ -194,7 +194,14 @@ export async function parseBody(
type ApiContext = __ApiPreviewProps & {
trustHostHeader?: boolean
allowedRevalidateHeaderKeys?: string[]
revalidate?: (_req: IncomingMessage, _res: ServerResponse) => Promise<any>
hostname?: string
revalidate?: (config: {
urlPath: string
revalidateHeaders: { [key: string]: string | string[] }
opts: { unstable_onlyGenerated?: boolean }
}) => Promise<any>

// (_req: IncomingMessage, _res: ServerResponse) => Promise<any>
}

function getMaxContentLength(responseLimit?: ResponseLimit) {
Expand Down Expand Up @@ -453,20 +460,44 @@ async function revalidate(
throw new Error(`Invalid response ${res.status}`)
}
} else if (context.revalidate) {
const mocked = createRequestResponseMocks({
url: urlPath,
headers: revalidateHeaders,
})
// We prefer to use the IPC call if running under the workers mode.
const ipcPort = process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT
if (ipcPort) {
const ipcKey = process.env.__NEXT_PRIVATE_ROUTER_IPC_KEY
const res = await invokeRequest(
`http://${
context.hostname
}:${ipcPort}?key=${ipcKey}&method=revalidate&args=${encodeURIComponent(
JSON.stringify([{ urlPath, revalidateHeaders }])
)}`,
{
method: 'GET',
headers: {},
}
)

await context.revalidate(mocked.req, mocked.res)
await mocked.res.hasStreamed
const chunks = []

if (
mocked.res.getHeader('x-nextjs-cache') !== 'REVALIDATED' &&
!(mocked.res.statusCode === 404 && opts.unstable_onlyGenerated)
) {
throw new Error(`Invalid response ${mocked.res.statusCode}`)
for await (const chunk of res) {
if (chunk) {
chunks.push(chunk)
}
}
const body = Buffer.concat(chunks).toString()
const result = JSON.parse(body)

if (result.err) {
throw new Error(result.err.message)
}

return
}

await context.revalidate({
urlPath,
revalidateHeaders,
opts,
})
} else {
throw new Error(
`Invariant: required internal revalidate method not passed to api-utils`
Expand Down
38 changes: 33 additions & 5 deletions packages/next/src/server/next-server.ts
Expand Up @@ -107,6 +107,7 @@ import { removePathPrefix } from '../shared/lib/router/utils/remove-path-prefix'
import { addPathPrefix } from '../shared/lib/router/utils/add-path-prefix'
import { pathHasPrefix } from '../shared/lib/router/utils/path-has-prefix'
import { filterReqHeaders, invokeRequest } from './lib/server-ipc'
import { createRequestResponseMocks } from './lib/mock-request'

export * from './base-server'

Expand Down Expand Up @@ -906,16 +907,13 @@ export default class NextNodeServer extends BaseServer {
pageModule,
{
...this.renderOpts.previewProps,
revalidate: (newReq: IncomingMessage, newRes: ServerResponse) =>
this.getRequestHandler()(
new NodeNextRequest(newReq),
new NodeNextResponse(newRes)
),
revalidate: this.revalidate.bind(this),
// internal config so is not typed
trustHostHeader: (this.nextConfig.experimental as Record<string, any>)
.trustHostHeader,
allowedRevalidateHeaderKeys:
this.nextConfig.experimental.allowedRevalidateHeaderKeys,
hostname: this.hostname,
},
this.minimalMode,
this.renderOpts.dev,
Expand Down Expand Up @@ -1672,6 +1670,36 @@ export default class NextNodeServer extends BaseServer {
}
}

public async revalidate({
urlPath,
revalidateHeaders,
opts,
}: {
urlPath: string
revalidateHeaders: { [key: string]: string | string[] }
opts: { unstable_onlyGenerated?: boolean }
}) {
const mocked = createRequestResponseMocks({
url: urlPath,
headers: revalidateHeaders,
})

const handler = this.getRequestHandler()
await handler(
new NodeNextRequest(mocked.req),
new NodeNextResponse(mocked.res)
)
await mocked.res.hasStreamed

if (
mocked.res.getHeader('x-nextjs-cache') !== 'REVALIDATED' &&
!(mocked.res.statusCode === 404 && opts.unstable_onlyGenerated)
) {
throw new Error(`Invalid response ${mocked.res.statusCode}`)
}
return {}
}

public async render(
req: BaseNextRequest | IncomingMessage,
res: BaseNextResponse | ServerResponse,
Expand Down
7 changes: 7 additions & 0 deletions test/production/app-dir/revalidate/app/layout.js
@@ -0,0 +1,7 @@
export default function Root({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
4 changes: 4 additions & 0 deletions test/production/app-dir/revalidate/app/page.js
@@ -0,0 +1,4 @@
export default async function Page() {
const data = Math.random()
return <h1>{data}</h1>
}
5 changes: 5 additions & 0 deletions test/production/app-dir/revalidate/next.config.js
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
appDir: true,
},
}
8 changes: 8 additions & 0 deletions test/production/app-dir/revalidate/pages/api/revalidate.js
@@ -0,0 +1,8 @@
export default async function (_req, res) {
try {
await res.revalidate('/')
return res.json({ revalidated: true })
} catch (err) {
return res.status(500).send('Error')
}
}
20 changes: 20 additions & 0 deletions test/production/app-dir/revalidate/revalidate.test.ts
@@ -0,0 +1,20 @@
import { createNextDescribe } from 'e2e-utils'

createNextDescribe(
'app-dir revalidate',
{
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('should be able to revalidate the cache via pages/api', async () => {
const $ = await next.render$('/')
const id = $('h1').text()
const res = await next.fetch('/api/revalidate')
expect(res.status).toBe(200)
const $2 = await next.render$('/')
const id2 = $2('h1').text()
expect(id).not.toBe(id2)
})
}
)

0 comments on commit 931c101

Please sign in to comment.