Skip to content

Commit ddf2af5

Browse files
authored
fix incorrect refresh request when basePath is set (#64589)
`initialCanonicalUrl` differs from the `canonicalUrl` that gets set on the client, such as when there's a basePath set. This hoists the `canonicalUrl` construction up so we can re-use it when adding refetch markers to the tree. This also renames `pathname` -> `path` since it also includes search params. I added a test to confirm no extra erroneous fetches happened in both cases. Fixes #64584 Closes NEXT-3130
1 parent e9aeb9e commit ddf2af5

File tree

5 files changed

+66
-21
lines changed

5 files changed

+66
-21
lines changed

packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function applyRouterStatePatchToTree(
8080
flightSegmentPath: FlightSegmentPath,
8181
flightRouterState: FlightRouterState,
8282
treePatch: FlightRouterState,
83-
pathname: string
83+
path: string
8484
): FlightRouterState | null {
8585
const [segment, parallelRoutes, url, refetch, isRootLayout] =
8686
flightRouterState
@@ -93,7 +93,7 @@ export function applyRouterStatePatchToTree(
9393
flightSegmentPath
9494
)
9595

96-
addRefreshMarkerToActiveParallelSegments(tree, pathname)
96+
addRefreshMarkerToActiveParallelSegments(tree, path)
9797

9898
return tree
9999
}
@@ -119,7 +119,7 @@ export function applyRouterStatePatchToTree(
119119
flightSegmentPath.slice(2),
120120
parallelRoutes[parallelRouteKey],
121121
treePatch,
122-
pathname
122+
path
123123
)
124124

125125
if (parallelRoutePatch === null) {
@@ -142,7 +142,7 @@ export function applyRouterStatePatchToTree(
142142
tree[4] = true
143143
}
144144

145-
addRefreshMarkerToActiveParallelSegments(tree, pathname)
145+
addRefreshMarkerToActiveParallelSegments(tree, path)
146146

147147
return tree
148148
}

packages/next/src/client/components/router-reducer/create-initial-router-state.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,15 @@ export function createInitialRouterState({
4949
loading: initialSeedData[3],
5050
}
5151

52-
addRefreshMarkerToActiveParallelSegments(initialTree, initialCanonicalUrl)
52+
const canonicalUrl =
53+
// location.href is read as the initial value for canonicalUrl in the browser
54+
// This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates in the useEffect further down in this file.
55+
location
56+
? // window.location does not have the same type as URL but has all the fields createHrefFromUrl needs.
57+
createHrefFromUrl(location)
58+
: initialCanonicalUrl
59+
60+
addRefreshMarkerToActiveParallelSegments(initialTree, canonicalUrl)
5361

5462
const prefetchCache = new Map<string, PrefetchCacheEntry>()
5563

@@ -82,13 +90,7 @@ export function createInitialRouterState({
8290
hashFragment: null,
8391
segmentPaths: [],
8492
},
85-
canonicalUrl:
86-
// location.href is read as the initial value for canonicalUrl in the browser
87-
// This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates in the useEffect further down in this file.
88-
location
89-
? // window.location does not have the same type as URL but has all the fields createHrefFromUrl needs.
90-
createHrefFromUrl(location)
91-
: initialCanonicalUrl,
93+
canonicalUrl,
9294
nextUrl:
9395
// the || operator is intentional, the pathname can be an empty string
9496
(extractPathFromFlightRouterState(initialTree) || location?.pathname) ??

packages/next/src/client/components/router-reducer/refetch-inactive-parallel-segments.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,23 @@ async function refreshInactiveParallelSegmentsImpl({
4545
fetchedSegments: Set<string>
4646
rootTree: FlightRouterState
4747
}) {
48-
const [, parallelRoutes, refetchPathname, refetchMarker] = updatedTree
48+
const [, parallelRoutes, refetchPath, refetchMarker] = updatedTree
4949
const fetchPromises = []
5050

5151
if (
52-
refetchPathname &&
53-
refetchPathname !== location.pathname &&
52+
refetchPath &&
53+
refetchPath !== location.pathname + location.search &&
5454
refetchMarker === 'refresh' &&
5555
// it's possible for the tree to contain multiple segments that contain data at the same URL
5656
// we keep track of them so we can dedupe the requests
57-
!fetchedSegments.has(refetchPathname)
57+
!fetchedSegments.has(refetchPath)
5858
) {
59-
fetchedSegments.add(refetchPathname) // Mark this URL as fetched
59+
fetchedSegments.add(refetchPath) // Mark this URL as fetched
6060

6161
// Eagerly kick off the fetch for the refetch path & the parallel routes. This should be fine to do as they each operate
6262
// independently on their own cache nodes, and `applyFlightData` will copy anything it doesn't care about from the existing cache.
6363
const fetchPromise = fetchServerResponse(
64-
new URL(refetchPathname, location.origin),
64+
new URL(refetchPath, location.origin),
6565
// refetch from the root of the updated tree, otherwise it will be scoped to the current segment
6666
// and might not contain the data we need to patch in interception route data (such as dynamic params from a previous segment)
6767
[rootTree[0], rootTree[1], rootTree[2], 'refetch'],
@@ -110,16 +110,16 @@ async function refreshInactiveParallelSegmentsImpl({
110110
*/
111111
export function addRefreshMarkerToActiveParallelSegments(
112112
tree: FlightRouterState,
113-
pathname: string
113+
path: string
114114
) {
115115
const [segment, parallelRoutes, , refetchMarker] = tree
116116
// a page segment might also contain concatenated search params, so we do a partial match on the key
117117
if (segment.includes(PAGE_SEGMENT_KEY) && refetchMarker !== 'refresh') {
118-
tree[2] = pathname
118+
tree[2] = path
119119
tree[3] = 'refresh'
120120
}
121121

122122
for (const key in parallelRoutes) {
123-
addRefreshMarkerToActiveParallelSegments(parallelRoutes[key], pathname)
123+
addRefreshMarkerToActiveParallelSegments(parallelRoutes[key], path)
124124
}
125125
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client'
2+
3+
import { useRouter } from 'next/navigation'
4+
5+
export default function Page() {
6+
const router = useRouter()
7+
return (
8+
<button
9+
onClick={() => {
10+
router.refresh()
11+
}}
12+
>
13+
Refresh
14+
</button>
15+
)
16+
}

test/e2e/app-dir/app-basepath/index.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,32 @@ createNextDescribe(
6464
expect(await browser.url()).toBe(`${next.url}/base/dynamic/dest`)
6565
})
6666
})
67+
68+
it.each(['/base/refresh', '/base/refresh?foo=bar'])(
69+
`should only make a single RSC call to the current page (%s)`,
70+
async (path) => {
71+
let rscRequests = []
72+
const browser = await next.browser(path, {
73+
beforePageLoad(page) {
74+
page.on('request', (request) => {
75+
return request.allHeaders().then((headers) => {
76+
if (
77+
headers['RSC'.toLowerCase()] === '1' &&
78+
// Prefetches also include `RSC`
79+
headers['Next-Router-Prefetch'.toLowerCase()] !== '1'
80+
) {
81+
rscRequests.push(request.url())
82+
}
83+
})
84+
})
85+
},
86+
})
87+
await browser.elementByCss('button').click()
88+
await retry(async () => {
89+
expect(rscRequests.length).toBe(1)
90+
expect(rscRequests[0]).toContain(`${next.url}${path}`)
91+
})
92+
}
93+
)
6794
}
6895
)

0 commit comments

Comments
 (0)