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

Fix cross-worker revalidate API #49101

Merged
merged 12 commits into from
May 3, 2023
57 changes: 44 additions & 13 deletions packages/next/src/server/api-utils/node.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
appDir: true,
},
}
8 changes: 8 additions & 0 deletions test/production/app-dir/revalidate/pages/api/revalidate.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
})
}
)