diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index dffb65244b408..1ab53f32acf8c 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -28,6 +28,7 @@ import { denormalizePagePath } from '../../../../shared/lib/page-path/denormaliz import cookie from 'next/dist/compiled/cookie' import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants' import { addRequestMeta } from '../../../../server/request-meta' +import { removeTrailingSlash } from '../../../../shared/lib/router/utils/remove-trailing-slash' export const vercelHeader = 'x-vercel-id' @@ -98,7 +99,11 @@ export function getUtils({ let fsPathname = parsedUrl.pathname const matchesPage = () => { - return fsPathname === page || dynamicRouteMatcher?.(fsPathname) + const fsPathnameNoSlash = removeTrailingSlash(fsPathname || '') + return ( + fsPathnameNoSlash === removeTrailingSlash(page) || + dynamicRouteMatcher?.(fsPathnameNoSlash) + ) } const checkRewrite = (rewrite: Rewrite): boolean => { diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index cccd5679389dc..c81c1175ed479 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1341,6 +1341,11 @@ export default abstract class Server { let resolvedUrlPathname = getRequestMeta(req, '_nextRewroteUrl') || urlPathname + if (isSSG && this.minimalMode && req.headers['x-matched-path']) { + // the url value is already correct when the matched-path header is set + resolvedUrlPathname = urlPathname + } + urlPathname = removeTrailingSlash(urlPathname) resolvedUrlPathname = normalizeLocalePath( removeTrailingSlash(resolvedUrlPathname), diff --git a/test/production/minimal-mode-response-cache/app/next.config.js b/test/production/minimal-mode-response-cache/app/next.config.js new file mode 100644 index 0000000000000..e882c72b0b633 --- /dev/null +++ b/test/production/minimal-mode-response-cache/app/next.config.js @@ -0,0 +1,32 @@ +module.exports = { + experimental: { + outputStandalone: true, + }, + trailingSlash: true, + rewrites() { + return { + beforeFiles: [ + { + source: '/news/:path/', + destination: '/news/:path*/', + }, + ], + afterFiles: [ + { + source: '/somewhere', + destination: '/', + }, + ], + fallback: [ + { + source: '/:path*', + destination: '/:path*', + }, + { + source: '/(.*)', + destination: '/seo-redirects', + }, + ], + } + }, +} diff --git a/test/production/minimal-mode-response-cache/app/pages/blog/[slug].js b/test/production/minimal-mode-response-cache/app/pages/blog/[slug].js new file mode 100644 index 0000000000000..7744865fab407 --- /dev/null +++ b/test/production/minimal-mode-response-cache/app/pages/blog/[slug].js @@ -0,0 +1,32 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + return ( + <> +

/blog/[slug]

+

{JSON.stringify(props)}

+

{router.asPath}

+

{router.pathname}

+

{JSON.stringify(router.query)}

+ + ) +} + +export function getStaticProps({ params }) { + console.log('getStaticProps /blog/[slug]', params) + return { + props: { + params, + now: Date.now(), + }, + } +} + +export function getStaticPaths() { + return { + paths: ['/blog/first'], + fallback: 'blocking', + } +} diff --git a/test/production/minimal-mode-response-cache/app/pages/index.js b/test/production/minimal-mode-response-cache/app/pages/index.js new file mode 100644 index 0000000000000..0d7ef7f93f621 --- /dev/null +++ b/test/production/minimal-mode-response-cache/app/pages/index.js @@ -0,0 +1,25 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + return ( + <> +

/

+

{JSON.stringify(props)}

+

{router.asPath}

+

{router.pathname}

+

{JSON.stringify(router.query)}

+ + ) +} + +export function getStaticProps() { + console.log('getStaticProps /') + return { + props: { + index: true, + now: Date.now(), + }, + } +} diff --git a/test/production/minimal-mode-response-cache/app/pages/news.js b/test/production/minimal-mode-response-cache/app/pages/news.js new file mode 100644 index 0000000000000..74ce52a740af5 --- /dev/null +++ b/test/production/minimal-mode-response-cache/app/pages/news.js @@ -0,0 +1,25 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + return ( + <> +

/news

+

{JSON.stringify(props)}

+

{router.asPath}

+

{router.pathname}

+

{JSON.stringify(router.query)}

+ + ) +} + +export function getStaticProps() { + console.log('getStaticProps /news') + return { + props: { + news: true, + now: Date.now(), + }, + } +} diff --git a/test/production/minimal-mode-response-cache/index.test.ts b/test/production/minimal-mode-response-cache/index.test.ts new file mode 100644 index 0000000000000..e311bfc8aaa4f --- /dev/null +++ b/test/production/minimal-mode-response-cache/index.test.ts @@ -0,0 +1,122 @@ +import glob from 'glob' +import fs from 'fs-extra' +import { join } from 'path' +import cheerio from 'cheerio' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { + killApp, + findPort, + renderViaHTTP, + initNextServerScript, +} from 'next-test-utils' + +describe('minimal-mode-response-cache', () => { + let next: NextInstance + let server + let appPort + + beforeAll(async () => { + // test build against environment with next support + process.env.NOW_BUILDER = '1' + + next = await createNext({ + files: new FileRef(join(__dirname, 'app')), + }) + await next.stop() + + await fs.move( + join(next.testDir, '.next/standalone'), + join(next.testDir, 'standalone') + ) + for (const file of await fs.readdir(next.testDir)) { + if (file !== 'standalone') { + await fs.remove(join(next.testDir, file)) + console.log('removed', file) + } + } + const files = glob.sync('**/*', { + cwd: join(next.testDir, 'standalone/.next/server/pages'), + dot: true, + }) + + for (const file of files) { + if (file.endsWith('.json') || file.endsWith('.html')) { + await fs.remove(join(next.testDir, '.next/server', file)) + } + } + + const testServer = join(next.testDir, 'standalone/server.js') + await fs.writeFile( + testServer, + (await fs.readFile(testServer, 'utf8')) + .replace('console.error(err)', `console.error('top-level', err)`) + .replace('conf:', 'minimalMode: true,conf:') + ) + appPort = await findPort() + server = await initNextServerScript( + testServer, + /Listening on/, + { + ...process.env, + PORT: appPort, + }, + undefined, + { + cwd: next.testDir, + } + ) + }) + afterAll(async () => { + await next.destroy() + if (server) await killApp(server) + }) + + it('should have correct responses', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html.length).toBeTruthy() + + for (const { path, matchedPath, query, asPath, pathname } of [ + { path: '/', asPath: '/' }, + { path: '/', matchedPath: '/index', asPath: '/' }, + { path: '/', matchedPath: '/index/', asPath: '/' }, + { path: '/', matchedPath: '/', asPath: '/' }, + { + path: '/news/', + matchedPath: '/news/', + asPath: '/news/', + pathname: '/news', + }, + { + path: '/news/', + matchedPath: '/news', + asPath: '/news/', + pathname: '/news', + }, + { + path: '/blog/first/', + matchedPath: '/blog/first/', + pathname: '/blog/[slug]', + asPath: '/blog/first/', + query: { slug: 'first' }, + }, + { + path: '/blog/second/', + matchedPath: '/blog/[slug]/', + pathname: '/blog/[slug]', + asPath: '/blog/second/', + query: { slug: 'second' }, + }, + ]) { + const html = await renderViaHTTP(appPort, path, undefined, { + headers: { + 'x-matched-path': matchedPath || path, + }, + }) + const $ = cheerio.load(html) + expect($('#asPath').text()).toBe(asPath) + expect($('#pathname').text()).toBe(pathname || path) + expect(JSON.parse($('#query').text())).toEqual(query || {}) + } + }) +})