Skip to content

Commit f12b55b

Browse files
committed
omit searchParam data from FlightRouterState before transport
1 parent 6afdb1b commit f12b55b

File tree

3 files changed

+100
-4
lines changed

3 files changed

+100
-4
lines changed

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { findSourceMapURL } from '../../app-find-source-map-url'
3333
import { PrefetchKind } from './router-reducer-types'
3434
import {
3535
normalizeFlightData,
36+
prepareFlightRouterStateForRequest,
3637
type NormalizedFlightData,
3738
} from '../../flight-data-helpers'
3839
import { getAppBuildId } from '../../app-build-id'
@@ -126,8 +127,9 @@ export async function fetchServerResponse(
126127
// Enable flight response
127128
[RSC_HEADER]: '1',
128129
// Provide the current router state
129-
[NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(
130-
JSON.stringify(flightRouterState)
130+
[NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(
131+
flightRouterState,
132+
options.isHmrRefresh
131133
),
132134
}
133135

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { handleSegmentMismatch } from '../handle-segment-mismatch'
4545
import { refreshInactiveParallelSegments } from '../refetch-inactive-parallel-segments'
4646
import {
4747
normalizeFlightData,
48+
prepareFlightRouterStateForRequest,
4849
type NormalizedFlightData,
4950
} from '../../../flight-data-helpers'
5051
import { getRedirectError } from '../../redirect'
@@ -92,8 +93,8 @@ async function fetchServerAction(
9293
headers: {
9394
Accept: RSC_CONTENT_TYPE_HEADER,
9495
[ACTION_HEADER]: actionId,
95-
[NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(
96-
JSON.stringify(state.tree)
96+
[NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(
97+
state.tree
9798
),
9899
...(process.env.NEXT_DEPLOYMENT_ID
99100
? {

packages/next/src/client/flight-data-helpers.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
Segment,
88
} from '../server/app-render/types'
99
import type { HeadData } from '../shared/lib/app-router-context.shared-runtime'
10+
import { PAGE_SEGMENT_KEY } from '../shared/lib/segment'
1011

1112
export type NormalizedFlightData = {
1213
/**
@@ -76,3 +77,95 @@ export function normalizeFlightData(
7677

7778
return flightData.map(getFlightDataPartsFromPath)
7879
}
80+
81+
/**
82+
* This function is used to prepare the flight router state for the request.
83+
* It removes markers that are not needed by the server, and are purely used
84+
* for stashing state on the client.
85+
* @param flightRouterState - The flight router state to prepare.
86+
* @param isHmrRefresh - Whether this is an HMR refresh request.
87+
* @returns The prepared flight router state.
88+
*/
89+
export function prepareFlightRouterStateForRequest(
90+
flightRouterState: FlightRouterState,
91+
isHmrRefresh?: boolean
92+
): string {
93+
// HMR requests need the complete, unmodified state for proper functionality
94+
if (isHmrRefresh) {
95+
return encodeURIComponent(JSON.stringify(flightRouterState))
96+
}
97+
98+
return encodeURIComponent(
99+
JSON.stringify(stripClientOnlyDataFromFlightRouterState(flightRouterState))
100+
)
101+
}
102+
103+
/**
104+
* Recursively strips client-only data from FlightRouterState while preserving
105+
* server-needed information for proper rendering decisions.
106+
*/
107+
function stripClientOnlyDataFromFlightRouterState(
108+
flightRouterState: FlightRouterState
109+
): FlightRouterState {
110+
const [
111+
segment,
112+
parallelRoutes,
113+
_url, // Intentionally unused - URLs are client-only
114+
refreshMarker,
115+
isRootLayout,
116+
hasLoadingBoundary,
117+
] = flightRouterState
118+
119+
// __PAGE__ segments are always fetched from the server, so there's
120+
// no need to send them up
121+
const cleanedSegment = stripSearchParamsFromPageSegment(segment)
122+
123+
// Recursively process parallel routes
124+
const cleanedParallelRoutes: { [key: string]: FlightRouterState } = {}
125+
for (const [key, childState] of Object.entries(parallelRoutes)) {
126+
cleanedParallelRoutes[key] =
127+
stripClientOnlyDataFromFlightRouterState(childState)
128+
}
129+
130+
const result: FlightRouterState = [
131+
cleanedSegment,
132+
cleanedParallelRoutes,
133+
null, // URLs omitted - server reconstructs paths from segments
134+
shouldPreserveRefreshMarker(refreshMarker) ? refreshMarker : null,
135+
]
136+
137+
// Append optional fields if present
138+
if (isRootLayout !== undefined) {
139+
result[4] = isRootLayout
140+
}
141+
if (hasLoadingBoundary !== undefined) {
142+
result[5] = hasLoadingBoundary
143+
}
144+
145+
return result
146+
}
147+
148+
/**
149+
* Strips search parameters from __PAGE__ segments to prevent sensitive
150+
* client-side data from being sent to the server.
151+
*/
152+
function stripSearchParamsFromPageSegment(segment: Segment): Segment {
153+
if (
154+
typeof segment === 'string' &&
155+
segment.startsWith(PAGE_SEGMENT_KEY + '?')
156+
) {
157+
return PAGE_SEGMENT_KEY
158+
}
159+
return segment
160+
}
161+
162+
/**
163+
* Determines whether the refresh marker should be sent to the server
164+
* Client-only markers like 'refresh' are stripped, while server-needed markers
165+
* like 'refetch' and 'inside-shared-layout' are preserved.
166+
*/
167+
function shouldPreserveRefreshMarker(
168+
refreshMarker: FlightRouterState[3]
169+
): boolean {
170+
return Boolean(refreshMarker && refreshMarker !== 'refresh')
171+
}

0 commit comments

Comments
 (0)