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(#33081): handle relative path correctly #36823

Merged
merged 9 commits into from
May 22, 2022
4 changes: 2 additions & 2 deletions packages/next/shared/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
NextPageContext,
ST,
NEXT_DATA,
isAbsoluteUrl,
} from '../utils'
import { isDynamicRoute } from './utils/is-dynamic'
import { parseRelativeUrl } from './utils/parse-relative-url'
Expand Down Expand Up @@ -231,8 +232,7 @@ export function delBasePath(path: string): string {
*/
export function isLocalURL(url: string): boolean {
// prevent a hydration mismatch on href for url with anchor refs
if (url.startsWith('/') || url.startsWith('#') || url.startsWith('?'))
return true
if (!isAbsoluteUrl(url)) return true
try {
// absolute urls can be local if they are on the same origin
const locationOrigin = getLocationOrigin()
Expand Down
8 changes: 7 additions & 1 deletion packages/next/shared/lib/router/utils/parse-relative-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ export function parseRelativeUrl(url: string, base?: string) {
const globalBase = new URL(
typeof window === 'undefined' ? 'http://n' : getLocationOrigin()
)
const resolvedBase = base ? new URL(base, globalBase) : globalBase

const resolvedBase = base
? new URL(base, globalBase)
: url.startsWith('.')
? new URL(typeof window === 'undefined' ? 'http://n' : window.location.href)
: globalBase

const { pathname, searchParams, search, hash, href, origin } = new URL(
url,
resolvedBase
Expand Down
5 changes: 5 additions & 0 deletions packages/next/shared/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ export function execOnce<T extends (...args: any[]) => ReturnType<T>>(
}) as T
}

// Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
// Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
const ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/
export const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url)

export function getLocationOrigin() {
const { protocol, hostname, port } = window.location
return `${protocol}//${hostname}${port ? ':' + port : ''}`
Expand Down
12 changes: 12 additions & 0 deletions test/integration/client-navigation/pages/nav/relative-1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link'

export default function Relative1() {
return (
<div id="relative-1">
On relative 1
<Link href="./relative-2">
<a>To relative 2</a>
</Link>
</div>
)
}
18 changes: 18 additions & 0 deletions test/integration/client-navigation/pages/nav/relative-2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useRouter } from 'next/router'

export default function Relative2() {
const router = useRouter()
return (
<div id="relative-2">
On relative 2
<button
onClick={(e) => {
e.preventDefault()
router.push('./relative')
}}
>
To relative index
</button>
</div>
)
}
12 changes: 12 additions & 0 deletions test/integration/client-navigation/pages/nav/relative/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link'

export default function Relative() {
return (
<div id="relative">
On relative index
<Link href="./relative-1" id="relative-1-link">
To relative 1
</Link>
</div>
)
}
24 changes: 24 additions & 0 deletions test/integration/client-navigation/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,30 @@ describe('Client Navigation', () => {
expect(value).toBe(false)
})

it('should navigate to paths relative to the current page', async () => {
const browser = await webdriver(context.appPort, '/nav/relative')
await browser.waitForElementByCss('body', 500)
let page

await browser.elementByCss('a').click()

browser.waitForElementByCss('#relative-1', 500)
page = await browser.elementByCss('body').text()
expect(page).toMatch(/On relative 1/)
await browser.elementByCss('a').click()

browser.waitForElementByCss('#relative-2', 500)
page = await browser.elementByCss('body').text()
expect(page).toMatch(/On relative 2/)

await browser.elementByCss('button').click()
browser.waitForElementByCss('#relative', 500)
page = await browser.elementByCss('body').text()
expect(page).toMatch(/On relative index/)

await browser.close()
})

renderingSuite(
(p, q) => renderViaHTTP(context.appPort, p, q),
(p, q) => fetchViaHTTP(context.appPort, p, q),
Expand Down
18 changes: 18 additions & 0 deletions test/unit/parse-relative-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ describe('parseRelativeUrl', () => {
check(
'http://example.com:3210/someA/pathB?fooC=barD#hashE',
'./someF/pathG?fooH=barI#hashJ',
{
pathname: '/someA/someF/pathG',
search: '?fooH=barI',
hash: '#hashJ',
}
)
check(
'http://example.com:3210/someA/pathB',
'../someF/pathG?fooH=barI#hashJ',
{
pathname: '/someF/pathG',
search: '?fooH=barI',
hash: '#hashJ',
}
)
check(
'http://example.com:3210/someA/pathB',
'../../someF/pathG?fooH=barI#hashJ',
{
pathname: '/someF/pathG',
search: '?fooH=barI',
Expand Down