Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/next/src/client/route-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
NEXT_RSC_UNION_QUERY,
} from './components/app-router-headers'
import { hasBasePath } from './has-base-path'
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
import { removeBasePath } from './remove-base-path'
import type {
NormalizedPathname,
Expand Down Expand Up @@ -225,9 +226,15 @@ export function urlToUrlWithoutFlightMarker(url: URL): URL {
urlWithoutFlightParameters.pathname.endsWith('.txt')
) {
const { pathname } = urlWithoutFlightParameters
const length = pathname.endsWith('/index.txt') ? 10 : 4
// Slice off `/index.txt` or `.txt` from the end of the pathname
urlWithoutFlightParameters.pathname = pathname.slice(0, -length)
// Undo the marker appended in `fetchServerResponse`, which is keyed on
// whether the requested pathname ended with a slash: `index.txt` for
// `/foo/`, `.txt` for `/foo`. Slicing off only `index.txt` keeps that
// slash, then `normalizePathTrailingSlash` applies the configured
// policy so we don't hand-roll a second one here.
const length = pathname.endsWith('/index.txt') ? 9 : 4
urlWithoutFlightParameters.pathname = normalizePathTrailingSlash(
pathname.slice(0, -length)
)
}
}
return urlWithoutFlightParameters
Expand Down
1 change: 1 addition & 0 deletions test/cache-components-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
"test/e2e/app-dir/server-source-maps/server-source-maps.test.ts",
"test/e2e/app-dir/set-cookies/set-cookies.test.ts",
"test/e2e/app-dir/shallow-routing/shallow-routing.test.ts",
"test/e2e/app-dir/static-export-skew-trailing-slash/static-export-skew-trailing-slash.test.ts",
"test/e2e/app-dir/static-generation-status/index.test.ts",
"test/e2e/app-dir/static-shell-debugging/static-shell-debugging.test.ts",
"test/e2e/app-dir/taint/process-taint.test.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
12 changes: 12 additions & 0 deletions test/e2e/app-dir/static-export-skew-trailing-slash/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link'

export default function Page() {
return (
<main>
<h1>Home page</h1>
<Link id="target-link" href="/target/" prefetch={false}>
Target page
</Link>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function TargetPage() {
return <h1 id="target-page">Target page</h1>
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/static-export-skew-trailing-slash/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
trailingSlash: true,
generateBuildId: async () => 'current-build-id',
}

module.exports = nextConfig
19 changes: 19 additions & 0 deletions test/e2e/app-dir/static-export-skew-trailing-slash/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createReadStream } from 'node:fs'
import { createServer } from 'node:http'
import { join } from 'node:path'
import handler from 'serve-handler'

export function createExportServer(outDir, requests) {
return createServer((request, response) => {
const { pathname } = new URL(request.url, 'http://localhost')
requests.push(pathname)

if (pathname === '/target' || pathname === '/target/') {
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
createReadStream(join(outDir, 'target/index.html')).pipe(response)
return
}

return handler(request, response, { public: outDir })
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Server } from 'node:http'
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { findPort, retry } from 'next-test-utils'
import { isNextStart, nextTestSetup } from 'e2e-utils'
import { createExportServer } from './server.mjs'

describe('static-export-skew-trailing-slash', () => {
if (!isNextStart) {
test('build test should not run during dev test run', () => {})
return
}

const { next } = nextTestSetup({
files: __dirname,
skipStart: true,
disableAutoSkewProtection: true,
})

let port: number
let server: Server
const requests: string[] = []

beforeAll(async () => {
await next.build()

const targetFlightPath = join(next.testDir, 'out/target/index.txt')
const targetFlight = readFileSync(targetFlightPath, 'utf8')
const currentBuildId = '"b":"current-build-id"'
if (!targetFlight.includes(currentBuildId)) {
throw new Error('Could not find the current build ID in target RSC data')
}
writeFileSync(
targetFlightPath,
targetFlight.replace(currentBuildId, '"b":"foreign-build-id"')
)

port = await findPort()
server = createExportServer(join(next.testDir, 'out'), requests)
server.listen(port)
})

afterAll(() => {
server?.close()
})

it('preserves the trailing slash during an MPA fallback', async () => {
const browser = await next.browser('/', { baseUrl: port })

await browser.elementById('target-link').click()
await browser.waitForElementByCss('#target-page')

await retry(async () => {
expect(new URL(await browser.url()).pathname).toBe('/target/')
})

expect(requests).toContain('/target/index.txt')
expect(
requests.filter(
(pathname) => pathname === '/target' || pathname === '/target/'
)
).toEqual(['/target/'])
})
})
Loading