diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts
index 8c18f4ec7f488cc6c420660b0c0f1c7b3cc5e99e..62e26fe066a22bfdfc9ad7d107bee4ffa8d2cb47 100644
--- a/packages/next/build/webpack/loaders/next-serverless-loader.ts
+++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts
@@ -299,6 +299,7 @@ const nextServerlessLoader: loader.Loader = function () {
const {parse: parseQs} = require('querystring')
const { renderToHTML } = require('next/dist/next-server/server/render');
const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils');
+ const { denormalizePagePath } = require('next/dist/next-server/server/denormalize-page-path')
const {sendPayload} = require('next/dist/next-server/server/send-payload');
const buildManifest = require('${buildManifest}');
const reactLoadableManifest = require('${reactLoadableManifest}');
@@ -465,8 +466,8 @@ const nextServerlessLoader: loader.Loader = function () {
: ''
}
- // normalize request URL/asPath for fallback pages since the proxy
- // sets the request URL to the output's path for fallback pages
+ // normalize request URL/asPath for fallback/revalidate pages since the
+ // proxy sets the request URL to the output's path for fallback pages
${
pageIsDynamicRoute
? `
@@ -482,12 +483,44 @@ const nextServerlessLoader: loader.Loader = function () {
_parsedUrl.pathname.substr(paramIdx + param.length + 2)
}
}
+ parsedUrl.pathname = _parsedUrl.pathname
req.url = formatUrl(_parsedUrl)
}
`
: ``
}
+ // make sure to normalize asPath for revalidate and _next/data requests
+ // since the asPath should match what is shown on the client
+ if (
+ !fromExport &&
+ (getStaticProps || getServerSideProps)
+ ) {
+ const curQuery = {...parsedUrl.query}
+
+ ${
+ pageIsDynamicRoute
+ ? `
+ // don't include dynamic route params in query while normalizing
+ // asPath
+ if (trustQuery) {
+ delete parsedUrl.search
+
+ for (const param of Object.keys(defaultRouteRegex.groups)) {
+ delete curQuery[param]
+ }
+ }
+ `
+ : ``
+ }
+
+ parsedUrl.pathname = denormalizePagePath(parsedUrl.pathname)
+ renderOpts.normalizedAsPath = formatUrl({
+ ...parsedUrl,
+ query: curQuery
+ })
+ }
+
const isFallback = parsedUrl.query.__nextFallback
const previewData = tryGetPreviewData(req, res, options.previewProps)
diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts
index 195931dfd871bf3096b4acae9781cc563f18a21e..a280a5423bc9166613ad15053d37bdf23414acd0 100644
--- a/packages/next/next-server/server/next-server.ts
+++ b/packages/next/next-server/server/next-server.ts
@@ -64,6 +64,7 @@ import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin
import { removePathTrailingSlash } from '../../client/normalize-trailing-slash'
import getRouteFromAssetPath from '../lib/router/utils/get-route-from-asset-path'
import { FontManifest } from './font-utils'
+import { denormalizePagePath } from './denormalize-page-path'
const getCustomRouteMatcher = pathMatch(true)
@@ -1025,9 +1026,9 @@ export default class Server {
// remove /_next/data prefix from urlPathname so it matches
// for direct page visit and /_next/data visit
if (isDataReq && urlPathname.includes(this.buildId)) {
- urlPathname = (urlPathname.split(this.buildId).pop() || '/')
- .replace(/\.json$/, '')
- .replace(/\/index$/, '/')
+ urlPathname = denormalizePagePath(
+ (urlPathname.split(this.buildId).pop() || '/').replace(/\.json$/, '')
+ )
}
const ssgCacheKey =
@@ -1110,7 +1111,15 @@ export default class Server {
...components,
...opts,
isDataReq,
+ normalizedAsPath: isDataReq
+ ? formatUrl({
+ pathname: urlPathname,
+ // make sure to only add query values from original URL
+ query: parseUrl(req.url!, true).query,
+ })
+ : undefined,
}
+
renderResult = await renderToHTML(
req,
res,
diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx
index df2be19398304e9eb15e5ddfc58b89aae814f442..f3fc22eede90ff316465904e6190fa28f8dd8c61 100644
--- a/packages/next/next-server/server/render.tsx
+++ b/packages/next/next-server/server/render.tsx
@@ -154,6 +154,7 @@ export type RenderOptsPartial = {
fontManifest?: FontManifest
optimizeImages: boolean
devOnlyCacheBusterQueryString?: string
+ normalizedAsPath?: string
}
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
@@ -488,7 +489,7 @@ export async function renderToHTML(
await Loadable.preloadAll() // Make sure all dynamic imports are loaded
// url will always be set
- const asPath: string = req.url as string
+ const asPath: string = renderOpts.normalizedAsPath || (req.url as string)
const router = new ServerRouter(
pathname,
query,
diff --git a/test/integration/getserversideprops/test/index.test.js b/test/integration/getserversideprops/test/index.test.js
index beb3164448b58505da4a2d5c7e77ae3bfa18f33e..1347346d4c40d2e4d1a172b2d061f6e5f6e6e49d 100644
--- a/test/integration/getserversideprops/test/index.test.js
+++ b/test/integration/getserversideprops/test/index.test.js
@@ -300,7 +300,7 @@ const runTests = (dev = false) => {
expect(appProps).toEqual({
url: curUrl,
query: { post: 'post-1' },
- asPath: curUrl,
+ asPath: '/blog/post-1',
pathname: '/blog/[post]',
})
})
@@ -313,7 +313,7 @@ const runTests = (dev = false) => {
expect(appProps).toEqual({
url: curUrl,
query: {},
- asPath: curUrl,
+ asPath: '/something',
pathname: '/something',
})
})
diff --git a/test/integration/revalidate-as-path/pages/_app.js b/test/integration/revalidate-as-path/pages/_app.js
new file mode 100644
index 0000000000000000000000000000000000000000..a3d16c84c9cccd7cff9aa3dad21d1a7344b1ff86
--- /dev/null
+++ b/test/integration/revalidate-as-path/pages/_app.js
@@ -0,0 +1,8 @@
+import { useRouter } from 'next/router'
+
+export default function App({ Component, pageProps }) {
+ // we log the asPath during rendering to make sure the value is
+ // correct during the /_next/data request since they are kept in sync
+ console.log(`asPath: ${useRouter().asPath}`)
+ return
{router.asPath}
+another
+ > + ) +} + +export const getStaticProps = () => { + return { + props: { + hello: 'world', + }, + revalidate: 1, + } +} diff --git a/test/integration/revalidate-as-path/pages/index.js b/test/integration/revalidate-as-path/pages/index.js new file mode 100644 index 0000000000000000000000000000000000000000..45196ef40afe3d6dfd24c05739edafa98f10b71e --- /dev/null +++ b/test/integration/revalidate-as-path/pages/index.js @@ -0,0 +1,21 @@ +import { useRouter } from 'next/router' + +export default function Index() { + const router = useRouter() + + return ( + <> +{router.asPath}
+index
+ > + ) +} + +export const getStaticProps = () => { + return { + props: { + hello: 'world', + }, + revalidate: 1, + } +} diff --git a/test/integration/revalidate-as-path/server.js b/test/integration/revalidate-as-path/server.js new file mode 100644 index 0000000000000000000000000000000000000000..0a75c6aa5a42509aacf8de17d64434f6e29ba774 --- /dev/null +++ b/test/integration/revalidate-as-path/server.js @@ -0,0 +1,38 @@ +const http = require('http') +const url = require('url') + +const render = (pagePath, req, res) => { + const mod = require(`./.next/serverless/pages/${pagePath}`) + return (mod.render || mod.default || mod)(req, res) +} + +const { BUILD_ID } = process.env + +const server = http.createServer(async (req, res) => { + try { + const { pathname } = url.parse(req.url) + + switch (pathname) { + case '/index': + case '/': + case `/_next/data/${BUILD_ID}/index.json`: + return render('index.js', req, res) + case '/another/index': + case '/another': + case `/_next/data/${BUILD_ID}/another/index.json`: + return render('another/index.js', req, res) + default: { + res.statusCode = 404 + res.end('not found') + } + } + } catch (err) { + console.error('failed to render', err) + res.statusCode = 500 + res.end(err.message) + } +}) + +server.listen(process.env.PORT, () => { + console.log('ready on', process.env.PORT) +}) diff --git a/test/integration/revalidate-as-path/test/index.test.js b/test/integration/revalidate-as-path/test/index.test.js new file mode 100644 index 0000000000000000000000000000000000000000..57b84db50d302b00af0ef4b948f039735cd6e568 --- /dev/null +++ b/test/integration/revalidate-as-path/test/index.test.js @@ -0,0 +1,140 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import cheerio from 'cheerio' +import { + killApp, + findPort, + nextBuild, + initNextServerScript, + renderViaHTTP, + nextStart, + waitFor, + check, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '../') +const nextConfigPath = join(appDir, 'next.config.js') +let appPort +let buildId +let app +let stdout = '' + +const checkAsPath = async (urlPath, expectedAsPath) => { + const html = await renderViaHTTP(appPort, urlPath) + const $ = cheerio.load(html) + const asPath = $('#as-path').text() + + expect(asPath).toBe(expectedAsPath) +} + +const runTests = (isServerless) => { + if (isServerless) { + it('should render with correct asPath with /index requested', async () => { + await checkAsPath('/index', '/') + }) + } + + it('should render with correct asPath with /_next/data /index requested', async () => { + stdout = '' + const path = `/_next/data/${buildId}/index.json` + await renderViaHTTP(appPort, path) + await waitFor(1000) + const data = await renderViaHTTP(appPort, path) + + expect(JSON.parse(data).pageProps).toEqual({ + hello: 'world', + }) + + await check(() => stdout, /asPath/) + const asPath = stdout.split('asPath: ').pop().split('\n').shift() + expect(asPath).toBe('/') + }) + + it('should render with correct asPath with / requested', async () => { + await checkAsPath('/', '/') + }) + + it('should render with correct asPath with /another/index requested', async () => { + await checkAsPath('/another/index', '/another/index') + }) + + it('should render with correct asPath with /_next/data /another/index requested', async () => { + stdout = '' + const path = `/_next/data/${buildId}/another/index.json` + await renderViaHTTP(appPort, path) + await waitFor(1000) + const data = await renderViaHTTP(appPort, path) + + expect(JSON.parse(data).pageProps).toEqual({ + hello: 'world', + }) + + await check(() => stdout, /asPath/) + const asPath = stdout.split('asPath: ').pop().split('\n').shift() + expect(asPath).toBe('/another/index') + }) +} + +describe('Revalidate asPath Normalizing', () => { + describe('raw serverless mode', () => { + beforeAll(async () => { + await fs.remove(join(appDir, '.next')) + await fs.writeFile( + nextConfigPath, + ` + module.exports = { + target: 'experimental-serverless-trace' + } + ` + ) + appPort = await findPort() + await nextBuild(appDir) + + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + + app = await initNextServerScript( + join(appDir, 'server.js'), + /ready on/, + { + ...process.env, + PORT: appPort, + BUILD_ID: buildId, + }, + /error/, + { + onStdout(msg) { + stdout += msg || '' + }, + } + ) + }) + afterAll(async () => { + await killApp(app) + await fs.remove(nextConfigPath) + }) + runTests() + }) + + describe('server mode', () => { + beforeAll(async () => { + await fs.remove(join(appDir, '.next')) + appPort = await findPort() + await nextBuild(appDir) + + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + + app = await nextStart(appDir, appPort, { + onStdout(msg) { + console.log('got stdout', msg) + stdout += msg || '' + }, + }) + }) + afterAll(() => killApp(app)) + runTests() + }) +})