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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ParsedRelativeUrl {
pathname: string
query: ParsedUrlQuery
search: string
slashes: undefined
}

/**
Expand Down Expand Up @@ -58,5 +59,8 @@ export function parseRelativeUrl(
search,
hash,
href: href.slice(origin.length),
// We don't know for relative URLs at this point since we set a custom, internal
// base that isn't surfaced to users.
slashes: undefined,
}
}
6 changes: 6 additions & 0 deletions packages/next/src/shared/lib/router/utils/parse-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ParsedUrl {
protocol?: string | null
query: ParsedUrlQuery
search: string
slashes: boolean | undefined
}

export function parseUrl(url: string): ParsedUrl {
Expand All @@ -29,5 +30,10 @@ export function parseUrl(url: string): ParsedUrl {
protocol: parsedURL.protocol,
query: searchParamsToUrlQuery(parsedURL.searchParams),
search: parsedURL.search,
slashes:
parsedURL.href.slice(
parsedURL.protocol.length,
parsedURL.protocol.length + 2
) === '//',
Comment on lines +33 to +37
Copy link
Member Author

@eps1lon eps1lon Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slashes is used by url.format and would've been set by the deprecated url.parse

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('parseDestination', () => {
"pathname": "/hello/:name",
"query": {},
"search": "",
"slashes": undefined,
}
`)
})
Expand All @@ -45,6 +46,7 @@ describe('parseDestination', () => {
"protocol": "https:",
"query": {},
"search": "",
"slashes": true,
}
`)
})
Expand Down Expand Up @@ -72,6 +74,7 @@ describe('parseDestination', () => {
"foo": ":bar",
},
"search": "?foo=:bar",
"slashes": true,
}
`)
})
Expand Down
8 changes: 6 additions & 2 deletions packages/next/src/shared/lib/router/utils/resolve-rewrites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { normalizeLocalePath } from '../../i18n/normalize-locale-path'
import { removeBasePath } from '../../../../client/remove-base-path'
import { parseRelativeUrl, type ParsedRelativeUrl } from './parse-relative-url'

interface ParsedAs extends Omit<ParsedRelativeUrl, 'slashes'> {
slashes: boolean | undefined
}

export default function resolveRewrites(
asPath: string,
pages: string[],
Expand All @@ -20,14 +24,14 @@ export default function resolveRewrites(
locales?: readonly string[]
): {
matchedPage: boolean
parsedAs: ParsedRelativeUrl
parsedAs: ParsedAs
asPath: string
resolvedHref?: string
externalDest?: boolean
} {
let matchedPage = false
let externalDest = false
let parsedAs = parseRelativeUrl(asPath)
let parsedAs: ParsedAs = parseRelativeUrl(asPath)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We later assign the return value of prepareDestination which just happened to structurally match ParsedRelativeUrl. Now it no longer does because ParsedRelativeUrl has undefined slashes while ParsedUrl has boolean slashes.

let fsPathname = removeTrailingSlash(
normalizeLocalePath(removeBasePath(parsedAs.pathname), locales).pathname
)
Expand Down
12 changes: 12 additions & 0 deletions test/e2e/app-dir/rewrites-redirects/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ module.exports = {
destination: '/config-redirect-catchall-after/:path*',
permanent: true,
},
{
source: '/config-redirect-itms-apps-slashes',
destination:
'itms-apps://apps.apple.com/de/app/xcode/id497799835?l=en-GB&mt=12',
permanent: true,
},
{
source: '/config-redirect-itms-apps-no-slashes',
destination:
'itms-apps:apps.apple.com/de/app/xcode/id497799835?l=en-GB&mt=12',
permanent: true,
},
]
},
}
22 changes: 22 additions & 0 deletions test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,26 @@ describe('redirects and rewrites', () => {
expect(url.pathname).toEndWith('-after')
})
})

it('redirects to exotic url schemes preserving slashes', async () => {
const response = await next.fetch('/config-redirect-itms-apps-slashes', {
redirect: 'manual',
})

expect(response.headers.get('location')).toEqual(
'itms-apps://apps.apple.com/de/app/xcode/id497799835?l=en-GB&mt=12'
)
expect(response.status).toBe(308)
})

it('redirects to exotic url schemes without adding unwanted slashes', async () => {
const response = await next.fetch('/config-redirect-itms-apps-no-slashes', {
redirect: 'manual',
})

expect(response.headers.get('location')).toEqual(
'itms-apps:apps.apple.com/de/app/xcode/id497799835?l=en-GB&mt=12'
)
expect(response.status).toBe(308)
})
})
Loading